package com.external.imagezoom; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import com.external.imagezoom.easing.Cubic; import com.external.imagezoom.easing.Easing; import com.external.imagezoom.graphics.FastBitmapDrawable; import com.external.imagezoom.utils.IDisposable; import com.insthub.BeeFramework.view.WebImageView; /** * Base View to manage image zoom/scrool/pinch operations * * @author alessandro * */ public abstract class ImageViewTouchBase extends WebImageView implements IDisposable { public interface OnDrawableChangeListener { /** * Callback invoked when a new drawable has been * assigned to the view * @param drawable */ void onDrawableChanged(Drawable drawable); }; public interface OnLayoutChangeListener { /** * Callback invoked when the layout bounds changed * @param changed * @param left * @param top * @param right * @param bottom */ void onLayoutChanged(boolean changed, int left, int top, int right, int bottom); }; /** * Use this to change the {@link com.external.imagezoom.ImageViewTouchBase#setDisplayType(com.external.imagezoom.ImageViewTouchBase.DisplayType)} of * this View * @author alessandro * */ public enum DisplayType { /** Image is not scaled by default */ NONE, /** Image will be always presented using this view's bounds */ FIT_TO_SCREEN, /** Image will be scaled only if bigger than the bounds of this view */ FIT_IF_BIGGER }; public static final String LOG_TAG = "ImageViewTouchBase"; protected static final boolean LOG_ENABLED = false; public static final float ZOOM_INVALID = -1f; protected Easing mEasing = new Cubic(); protected Matrix mBaseMatrix = new Matrix(); protected Matrix mSuppMatrix = new Matrix(); protected Matrix mNextMatrix; protected Handler mHandler = new Handler(); protected Runnable mLayoutRunnable = null; protected boolean mUserScaled = false; private float mMaxZoom = ZOOM_INVALID; private float mMinZoom = ZOOM_INVALID; // true when min and max zoom are explicitly defined private boolean mMaxZoomDefined; private boolean mMinZoomDefined; protected final Matrix mDisplayMatrix = new Matrix(); protected final float[] mMatrixValues = new float[9]; private int mThisWidth = -1; private int mThisHeight = -1; private PointF mCenter = new PointF(); protected DisplayType mScaleType = DisplayType.NONE; private boolean mScaleTypeChanged; private boolean mBitmapChanged; final protected int DEFAULT_ANIMATION_DURATION = 200; protected RectF mBitmapRect = new RectF(); protected RectF mCenterRect = new RectF(); protected RectF mScrollRect = new RectF(); private OnDrawableChangeListener mDrawableChangeListener; private OnLayoutChangeListener mOnLayoutChangeListener; public ImageViewTouchBase( Context context ) { this( context, null ); } public ImageViewTouchBase( Context context, AttributeSet attrs ) { this( context, attrs, 0 ); } public ImageViewTouchBase( Context context, AttributeSet attrs, int defStyle ) { super( context, attrs, defStyle ); init( context, attrs, defStyle ); } public void setOnDrawableChangedListener( OnDrawableChangeListener listener ) { mDrawableChangeListener = listener; } public void setOnLayoutChangeListener( OnLayoutChangeListener listener ) { mOnLayoutChangeListener = listener; } protected void init( Context context, AttributeSet attrs, int defStyle ) { setScaleType( ScaleType.MATRIX ); } @Override public void setScaleType( ScaleType scaleType ) { if ( scaleType == ScaleType.MATRIX ) { super.setScaleType( scaleType ); } else { Log.w( LOG_TAG, "Unsupported scaletype. Only MATRIX can be used" ); } } /** * Clear the current drawable */ public void clear() { setImageBitmap( null ); } /** * Change the display type */ public void setDisplayType( DisplayType type ) { if ( type != mScaleType ) { if ( LOG_ENABLED ) { Log.i( LOG_TAG, "setDisplayType: " + type ); } mUserScaled = false; mScaleType = type; mScaleTypeChanged = true; requestLayout(); } } public DisplayType getDisplayType() { return mScaleType; } protected void setMinScale( float value ) { if ( LOG_ENABLED ) { Log.d( LOG_TAG, "setMinZoom: " + value ); } mMinZoom = value; } protected void setMaxScale( float value ) { if ( LOG_ENABLED ) { Log.d( LOG_TAG, "setMaxZoom: " + value ); } mMaxZoom = value; } @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom ) { if ( LOG_ENABLED ) { Log.e( LOG_TAG, "onLayout: " + changed + ", bitmapChanged: " + mBitmapChanged + ", scaleChanged: " + mScaleTypeChanged ); } super.onLayout( changed, left, top, right, bottom ); int deltaX = 0; int deltaY = 0; if( changed ) { int oldw = mThisWidth; int oldh = mThisHeight; mThisWidth = right - left; mThisHeight = bottom - top; deltaX = mThisWidth - oldw; deltaY = mThisHeight - oldh; // update center point mCenter.x = mThisWidth / 2f; mCenter.y = mThisHeight / 2f; } Runnable r = mLayoutRunnable; if ( r != null ) { mLayoutRunnable = null; r.run(); } final Drawable drawable = getDrawable(); if ( drawable != null ) { if ( changed || mScaleTypeChanged || mBitmapChanged ) { float scale = 1; // retrieve the old values float old_default_scale = getDefaultScale( mScaleType ); float old_matrix_scale = getScale( mBaseMatrix ); float old_scale = getScale(); float old_min_scale = Math.min( 1f, 1f / old_matrix_scale ); getProperBaseMatrix( drawable, mBaseMatrix ); float new_matrix_scale = getScale( mBaseMatrix ); if( LOG_ENABLED ) { Log.d( LOG_TAG, "old matrix scale: " + old_matrix_scale ); Log.d( LOG_TAG, "new matrix scale: " + new_matrix_scale ); Log.d( LOG_TAG, "old min scale: " + old_min_scale ); Log.d( LOG_TAG, "old scale: " + old_scale ); } // 1. bitmap changed or scaletype changed if ( mBitmapChanged || mScaleTypeChanged ) { if( LOG_ENABLED ) { Log.d( LOG_TAG, "display type: " + mScaleType ); Log.d( LOG_TAG, "newMatrix: " + mNextMatrix ); } if ( mNextMatrix != null ) { mSuppMatrix.set( mNextMatrix ); mNextMatrix = null; scale = getScale(); } else { mSuppMatrix.reset(); scale = getDefaultScale( mScaleType ); } setImageMatrix( getImageViewMatrix() ); if ( scale != getScale() ) { zoomTo( scale ); } } else if ( changed ) { // 2. layout size changed if ( !mMinZoomDefined ) mMinZoom = ZOOM_INVALID; if ( !mMaxZoomDefined ) mMaxZoom = ZOOM_INVALID; setImageMatrix( getImageViewMatrix() ); postTranslate( -deltaX, -deltaY ); if( !mUserScaled ) { scale = getDefaultScale( mScaleType ); zoomTo( scale ); } else { if( Math.abs( old_scale - old_min_scale ) > 0.001 ) { scale = ( old_matrix_scale / new_matrix_scale ) * old_scale; } zoomTo( scale ); } if ( LOG_ENABLED ) { Log.d( LOG_TAG, "old min scale: " + old_default_scale ); Log.d( LOG_TAG, "old scale: " + old_scale ); Log.d( LOG_TAG, "new scale: " + scale ); } } mUserScaled = false; if ( scale > getMaxScale() || scale < getMinScale() ) { // if current scale if outside the min/max bounds // then restore the correct scale zoomTo( scale ); } center( true, true ); if( mBitmapChanged ) onDrawableChanged( drawable ); if( changed || mBitmapChanged || mScaleTypeChanged ) onLayoutChanged( left, top, right, bottom ); if ( mScaleTypeChanged ) mScaleTypeChanged = false; if ( mBitmapChanged ) mBitmapChanged = false; if( LOG_ENABLED ) { Log.d( LOG_TAG, "new scale: " + getScale() ); } } } else { // drawable is null if( mBitmapChanged ) onDrawableChanged( drawable ); if( changed || mBitmapChanged || mScaleTypeChanged ) onLayoutChanged( left, top, right, bottom ); if ( mBitmapChanged ) mBitmapChanged = false; if ( mScaleTypeChanged ) mScaleTypeChanged = false; } } /** * Restore the original display * */ public void resetDisplay() { mBitmapChanged = true; requestLayout(); } public void resetMatrix() { if( LOG_ENABLED ) { Log.i( LOG_TAG, "resetMatrix" ); } mSuppMatrix = new Matrix(); float scale = getDefaultScale( mScaleType ); setImageMatrix( getImageViewMatrix() ); if( LOG_ENABLED ) { Log.d( LOG_TAG, "default scale: " + scale + ", scale: " + getScale() ); } if ( scale != getScale() ) { zoomTo( scale ); } postInvalidate(); } protected float getDefaultScale( DisplayType type ) { if ( type == DisplayType.FIT_TO_SCREEN ) { // always fit to screen return 1f; } else if( type == DisplayType.FIT_IF_BIGGER ) { // normal scale if smaller, fit to screen otherwise return Math.min( 1f, 1f / getScale( mBaseMatrix ) ); } else { // no scale return 1f / getScale( mBaseMatrix ); } } @Override public void setImageResource( int resId ) { setImageDrawable( getContext().getResources().getDrawable( resId ) ); } /** * {@inheritDoc} Set the new image to display and reset the internal matrix. * * @param bitmap * the {@link android.graphics.Bitmap} to display * @see {@link android.widget.ImageView#setImageBitmap(android.graphics.Bitmap)} */ @Override public void setImageBitmap( final Bitmap bitmap ) { setImageBitmap( bitmap, null, ZOOM_INVALID, ZOOM_INVALID ); } /** * @see #setImageDrawable(android.graphics.drawable.Drawable, android.graphics.Matrix, float, float) * * @param bitmap * @param matrix * @param min_zoom * @param max_zoom */ public void setImageBitmap( final Bitmap bitmap, Matrix matrix, float min_zoom, float max_zoom ) { if ( bitmap != null ) setImageDrawable( new FastBitmapDrawable( bitmap ), matrix, min_zoom, max_zoom ); else setImageDrawable( null, matrix, min_zoom, max_zoom ); } @Override public void setImageDrawable( Drawable drawable ) { setImageDrawable( drawable, null, ZOOM_INVALID, ZOOM_INVALID ); } /** * * Note: if the scaleType is FitToScreen then min_zoom must be <= 1 and max_zoom must be >= 1 * * @param drawable * the new drawable * @param initial_matrix * the optional initial display matrix * @param min_zoom * the optional minimum scale, pass {@link #ZOOM_INVALID} to use the default min_zoom * @param max_zoom * the optional maximum scale, pass {@link #ZOOM_INVALID} to use the default max_zoom */ public void setImageDrawable( final Drawable drawable, final Matrix initial_matrix, final float min_zoom, final float max_zoom ) { final int viewWidth = getWidth(); if ( viewWidth <= 0 ) { mLayoutRunnable = new Runnable() { @Override public void run() { setImageDrawable( drawable, initial_matrix, min_zoom, max_zoom ); } }; return; } _setImageDrawable( drawable, initial_matrix, min_zoom, max_zoom ); } protected void _setImageDrawable( final Drawable drawable, final Matrix initial_matrix, float min_zoom, float max_zoom ) { if ( LOG_ENABLED ) { Log.i( LOG_TAG, "_setImageDrawable" ); } if ( drawable != null ) { if ( LOG_ENABLED ) { Log.d( LOG_TAG, "size: " + drawable.getIntrinsicWidth() + "x" + drawable.getIntrinsicHeight() ); } super.setImageDrawable( drawable ); } else { mBaseMatrix.reset(); super.setImageDrawable( null ); } if ( min_zoom != ZOOM_INVALID && max_zoom != ZOOM_INVALID ) { min_zoom = Math.min( min_zoom, max_zoom ); max_zoom = Math.max( min_zoom, max_zoom ); mMinZoom = min_zoom; mMaxZoom = max_zoom; mMinZoomDefined = true; mMaxZoomDefined = true; if ( mScaleType == DisplayType.FIT_TO_SCREEN || mScaleType == DisplayType.FIT_IF_BIGGER ) { if ( mMinZoom >= 1 ) { mMinZoomDefined = false; mMinZoom = ZOOM_INVALID; } if ( mMaxZoom <= 1 ) { mMaxZoomDefined = true; mMaxZoom = ZOOM_INVALID; } } } else { mMinZoom = ZOOM_INVALID; mMaxZoom = ZOOM_INVALID; mMinZoomDefined = false; mMaxZoomDefined = false; } if ( initial_matrix != null ) { mNextMatrix = new Matrix( initial_matrix ); } mBitmapChanged = true; requestLayout(); } /** * Fired as soon as a new Bitmap has been set * * @param drawable */ protected void onDrawableChanged( final Drawable drawable ) { if( LOG_ENABLED ) { Log.i( LOG_TAG, "onDrawableChanged" ); } fireOnDrawableChangeListener( drawable ); } protected void fireOnLayoutChangeListener( int left, int top, int right, int bottom ) { if( null != mOnLayoutChangeListener ) { mOnLayoutChangeListener.onLayoutChanged( true, left, top, right, bottom ); } } protected void fireOnDrawableChangeListener( Drawable drawable ) { if( null != mDrawableChangeListener ) { mDrawableChangeListener.onDrawableChanged( drawable ); } } /** * Called just after {@link #onLayout(boolean, int, int, int, int)} * if the view's bounds has changed or a new Drawable has been set * or the {@link com.external.imagezoom.ImageViewTouchBase.DisplayType} has been modified * * @param left * @param top * @param right * @param bottom */ protected void onLayoutChanged( int left, int top, int right, int bottom ) { if( LOG_ENABLED ) { Log.i( LOG_TAG, "onLayoutChanged" ); } fireOnLayoutChangeListener( left, top, right, bottom ); } protected float computeMaxZoom() { final Drawable drawable = getDrawable(); if ( drawable == null ) { return 1F; } float fw = (float) drawable.getIntrinsicWidth() / (float) mThisWidth; float fh = (float) drawable.getIntrinsicHeight() / (float) mThisHeight; float scale = Math.max( fw, fh ) * 8; if ( LOG_ENABLED ) { Log.i( LOG_TAG, "computeMaxZoom: " + scale ); } return scale; } protected float computeMinZoom() { final Drawable drawable = getDrawable(); if ( drawable == null ) { return 1F; } float scale = getScale( mBaseMatrix ); scale = Math.min( 1f, 1f / scale ); if ( LOG_ENABLED ) { Log.i( LOG_TAG, "computeMinZoom: " + scale ); } return scale; } /** * Returns the current maximum allowed image scale * * @return */ public float getMaxScale() { if ( mMaxZoom == ZOOM_INVALID ) { mMaxZoom = computeMaxZoom(); } return mMaxZoom; } /** * Returns the current minimum allowed image scale * * @return */ public float getMinScale() { if ( mMinZoom == ZOOM_INVALID ) { mMinZoom = computeMinZoom(); } return mMinZoom; } /** * Returns the current view matrix * * @return */ public Matrix getImageViewMatrix() { return getImageViewMatrix( mSuppMatrix ); } public Matrix getImageViewMatrix( Matrix supportMatrix ) { mDisplayMatrix.set( mBaseMatrix ); mDisplayMatrix.postConcat( supportMatrix ); return mDisplayMatrix; } @Override public void setImageMatrix( Matrix matrix ) { Matrix current = getImageMatrix(); boolean needUpdate = false; if ( matrix == null && !current.isIdentity() || matrix != null && !current.equals( matrix ) ) { needUpdate = true; } super.setImageMatrix( matrix ); if ( needUpdate ) onImageMatrixChanged(); } /** * Called just after a new Matrix has been assigned. * * @see {@link #setImageMatrix(android.graphics.Matrix)} */ protected void onImageMatrixChanged() {} /** * Returns the current image display matrix.<br /> * This matrix can be used in the next call to the {@link #setImageDrawable(android.graphics.drawable.Drawable, android.graphics.Matrix, float, float)} to restore the same * view state of the previous {@link android.graphics.Bitmap}.<br /> * Example: * * <pre> * Matrix currentMatrix = mImageView.getDisplayMatrix(); * mImageView.setImageBitmap( newBitmap, currentMatrix, ZOOM_INVALID, ZOOM_INVALID ); * </pre> * * @return the current support matrix */ public Matrix getDisplayMatrix() { return new Matrix( mSuppMatrix ); } /** * Setup the base matrix so that the image is centered and scaled properly. * * @param drawable * @param matrix */ protected void getProperBaseMatrix( Drawable drawable, Matrix matrix ) { float viewWidth = mThisWidth; float viewHeight = mThisHeight; if ( LOG_ENABLED ) { Log.d( LOG_TAG, "getProperBaseMatrix. view: " + viewWidth + "x" + viewHeight ); } float w = drawable.getIntrinsicWidth(); float h = drawable.getIntrinsicHeight(); float widthScale, heightScale; matrix.reset(); if ( w > viewWidth || h > viewHeight ) { widthScale = viewWidth / w; heightScale = viewHeight / h; float scale = Math.min( widthScale, heightScale ); matrix.postScale( scale, scale ); float tw = ( viewWidth - w * scale ) / 2.0f; float th = ( viewHeight - h * scale ) / 2.0f; matrix.postTranslate( tw, th ); } else { widthScale = viewWidth / w; heightScale = viewHeight / h; float scale = Math.min( widthScale, heightScale ); matrix.postScale( scale, scale ); float tw = ( viewWidth - w * scale ) / 2.0f; float th = ( viewHeight - h * scale ) / 2.0f; matrix.postTranslate( tw, th ); } if( LOG_ENABLED ) { printMatrix( matrix ); } } /** * Setup the base matrix so that the image is centered and scaled properly. * * @param bitmap * @param matrix */ protected void getProperBaseMatrix2( Drawable bitmap, Matrix matrix ) { float viewWidth = mThisWidth; float viewHeight = mThisHeight; float w = bitmap.getIntrinsicWidth(); float h = bitmap.getIntrinsicHeight(); matrix.reset(); float widthScale = viewWidth / w; float heightScale = viewHeight / h; float scale = Math.min( widthScale, heightScale ); matrix.postScale( scale, scale ); matrix.postTranslate( ( viewWidth - w * scale ) / 2.0f, ( viewHeight - h * scale ) / 2.0f ); } protected float getValue( Matrix matrix, int whichValue ) { matrix.getValues( mMatrixValues ); return mMatrixValues[whichValue]; } public void printMatrix( Matrix matrix ) { float scalex = getValue( matrix, Matrix.MSCALE_X ); float scaley = getValue( matrix, Matrix.MSCALE_Y ); float tx = getValue( matrix, Matrix.MTRANS_X ); float ty = getValue( matrix, Matrix.MTRANS_Y ); Log.d( LOG_TAG, "matrix: { x: " + tx + ", y: " + ty + ", scalex: " + scalex + ", scaley: " + scaley + " }" ); } public RectF getBitmapRect() { return getBitmapRect( mSuppMatrix ); } protected RectF getBitmapRect( Matrix supportMatrix ) { final Drawable drawable = getDrawable(); if ( drawable == null ) return null; Matrix m = getImageViewMatrix( supportMatrix ); mBitmapRect.set( 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() ); m.mapRect( mBitmapRect ); return mBitmapRect; } protected float getScale( Matrix matrix ) { return getValue( matrix, Matrix.MSCALE_X ); } @SuppressLint("Override") public float getRotation() { return 0; } /** * Returns the current image scale * * @return */ public float getScale() { return getScale( mSuppMatrix ); } public float getBaseScale() { return getScale( mBaseMatrix ); } protected void center( boolean horizontal, boolean vertical ) { final Drawable drawable = getDrawable(); if ( drawable == null ) return; RectF rect = getCenter( mSuppMatrix, horizontal, vertical ); if ( rect.left != 0 || rect.top != 0 ) { if ( LOG_ENABLED ) { Log.i( LOG_TAG, "center" ); } postTranslate( rect.left, rect.top ); } } protected RectF getCenter( Matrix supportMatrix, boolean horizontal, boolean vertical ) { final Drawable drawable = getDrawable(); if ( drawable == null ) return new RectF( 0, 0, 0, 0 ); mCenterRect.set( 0, 0, 0, 0 ); RectF rect = getBitmapRect( supportMatrix ); float height = rect.height(); float width = rect.width(); float deltaX = 0, deltaY = 0; if ( vertical ) { int viewHeight = mThisHeight; if ( height < viewHeight ) { deltaY = ( viewHeight - height ) / 2 - rect.top; } else if ( rect.top > 0 ) { deltaY = -rect.top; } else if ( rect.bottom < viewHeight ) { deltaY = mThisHeight - rect.bottom; } } if ( horizontal ) { int viewWidth = mThisWidth; if ( width < viewWidth ) { deltaX = ( viewWidth - width ) / 2 - rect.left; } else if ( rect.left > 0 ) { deltaX = -rect.left; } else if ( rect.right < viewWidth ) { deltaX = viewWidth - rect.right; } } mCenterRect.set( deltaX, deltaY, 0, 0 ); return mCenterRect; } protected void postTranslate( float deltaX, float deltaY ) { if ( deltaX != 0 || deltaY != 0 ) { if ( LOG_ENABLED ) { Log.i( LOG_TAG, "postTranslate: " + deltaX + "x" + deltaY ); } mSuppMatrix.postTranslate( deltaX, deltaY ); setImageMatrix( getImageViewMatrix() ); } } protected void postScale( float scale, float centerX, float centerY ) { if ( LOG_ENABLED ) { Log.i( LOG_TAG, "postScale: " + scale + ", center: " + centerX + "x" + centerY ); } mSuppMatrix.postScale( scale, scale, centerX, centerY ); setImageMatrix( getImageViewMatrix() ); } protected PointF getCenter() { return mCenter; } protected void zoomTo( float scale ) { if( LOG_ENABLED ) { Log.i( LOG_TAG, "zoomTo: " + scale ); } if ( scale > getMaxScale() ) scale = getMaxScale(); if ( scale < getMinScale() ) scale = getMinScale(); if( LOG_ENABLED ) { Log.d( LOG_TAG, "sanitized scale: " + scale ); } PointF center = getCenter(); zoomTo( scale, center.x, center.y ); } /** * Scale to the target scale * * @param scale * the target zoom * @param durationMs * the animation duration */ public void zoomTo( float scale, float durationMs ) { PointF center = getCenter(); zoomTo( scale, center.x, center.y, durationMs ); } protected void zoomTo( float scale, float centerX, float centerY ) { if ( scale > getMaxScale() ) scale = getMaxScale(); float oldScale = getScale(); float deltaScale = scale / oldScale; postScale( deltaScale, centerX, centerY ); onZoom( getScale() ); center( true, true ); } protected void onZoom( float scale ){} protected void onZoomAnimationCompleted( float scale ){} /** * Scrolls the view by the x and y amount * * @param x * @param y */ public void scrollBy( float x, float y ) { panBy( x, y ); } protected void panBy( double dx, double dy ) { RectF rect = getBitmapRect(); mScrollRect.set( (float) dx, (float) dy, 0, 0 ); updateRect( rect, mScrollRect ); postTranslate( mScrollRect.left, mScrollRect.top ); center( true, true ); } protected void updateRect( RectF bitmapRect, RectF scrollRect ) { if ( bitmapRect == null ) return; if ( bitmapRect.top >= 0 && bitmapRect.bottom <= mThisHeight ) scrollRect.top = 0; if ( bitmapRect.left >= 0 && bitmapRect.right <= mThisWidth ) scrollRect.left = 0; if ( bitmapRect.top + scrollRect.top >= 0 && bitmapRect.bottom > mThisHeight ) scrollRect.top = (int) ( 0 - bitmapRect.top ); if ( bitmapRect.bottom + scrollRect.top <= ( mThisHeight - 0 ) && bitmapRect.top < 0 ) scrollRect.top = (int) ( ( mThisHeight - 0 ) - bitmapRect.bottom ); if ( bitmapRect.left + scrollRect.left >= 0 ) scrollRect.left = (int) ( 0 - bitmapRect.left ); if ( bitmapRect.right + scrollRect.left <= ( mThisWidth - 0 ) ) scrollRect.left = (int) ( ( mThisWidth - 0 ) - bitmapRect.right ); } protected void scrollBy( float distanceX, float distanceY, final double durationMs ) { final double dx = distanceX; final double dy = distanceY; final long startTime = System.currentTimeMillis(); mHandler.post( new Runnable() { double old_x = 0; double old_y = 0; @Override public void run() { long now = System.currentTimeMillis(); double currentMs = Math.min( durationMs, now - startTime ); double x = mEasing.easeOut( currentMs, 0, dx, durationMs ); double y = mEasing.easeOut( currentMs, 0, dy, durationMs ); panBy( ( x - old_x ), ( y - old_y ) ); old_x = x; old_y = y; if ( currentMs < durationMs ) { mHandler.post( this ); } else { RectF centerRect = getCenter( mSuppMatrix, true, true ); if ( centerRect.left != 0 || centerRect.top != 0 ) scrollBy( centerRect.left, centerRect.top ); } } } ); } protected void zoomTo( float scale, float centerX, float centerY, final float durationMs ) { if ( scale > getMaxScale() ) scale = getMaxScale(); final long startTime = System.currentTimeMillis(); final float oldScale = getScale(); final float deltaScale = scale - oldScale; Matrix m = new Matrix( mSuppMatrix ); m.postScale( scale, scale, centerX, centerY ); RectF rect = getCenter( m, true, true ); final float destX = centerX + rect.left * scale; final float destY = centerY + rect.top * scale; mHandler.post( new Runnable() { @Override public void run() { long now = System.currentTimeMillis(); float currentMs = Math.min( durationMs, now - startTime ); float newScale = (float) mEasing.easeInOut( currentMs, 0, deltaScale, durationMs ); zoomTo( oldScale + newScale, destX, destY ); if ( currentMs < durationMs ) { mHandler.post( this ); } else { onZoomAnimationCompleted( getScale() ); center( true, true ); } } } ); } @Override public void dispose() { clear(); } }