package it.sephiroth.android.library.imagezoom; import it.sephiroth.android.library.imagezoom.easing.Cubic; import it.sephiroth.android.library.imagezoom.easing.Easing; import it.sephiroth.android.library.imagezoom.graphics.FastBitmapDrawable; import it.sephiroth.android.library.imagezoom.utils.IDisposable; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Matrix; 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; /** * Base View to manage image zoom/scrool/pinch operations * * @author alessandro * */ public class ImageViewTouchBase extends ImageView implements IDisposable { public interface OnBitmapChangedListener { void onBitmapChanged( Drawable drawable ); }; public static final String LOG_TAG = "image"; protected Easing mEasing = new Cubic(); protected Matrix mBaseMatrix = new Matrix(); protected Matrix mSuppMatrix = new Matrix(); protected Handler mHandler = new Handler(); protected Runnable mOnLayoutRunnable = null; protected float mMaxZoom; protected final Matrix mDisplayMatrix = new Matrix(); protected final float[] mMatrixValues = new float[9]; protected int mThisWidth = -1, mThisHeight = -1; protected boolean mFitToScreen = false; protected boolean mFitToWidth = false; final protected float MAX_ZOOM = 5.0f; protected RectF mBitmapRect = new RectF(); protected RectF mCenterRect = new RectF(); protected RectF mScrollRect = new RectF(); private OnBitmapChangedListener mListener; public ImageViewTouchBase( Context context ) { super( context ); init(); } public ImageViewTouchBase( Context context, AttributeSet attrs ) { super( context, attrs ); init(); } public void setOnBitmapChangedListener( OnBitmapChangedListener listener ) { mListener = listener; } protected void init() { setScaleType( ImageView.ScaleType.MATRIX ); } public void clear() { setImageBitmap( null, true ); } public void setFitToScreen( boolean value ) { if ( value != mFitToScreen ) { mFitToScreen = value; requestLayout(); } } public void setFitToWidth( boolean value ) { if ( value != mFitToWidth ) { mFitToWidth = value; requestLayout(); } } @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom ) { super.onLayout( changed, left, top, right, bottom ); mThisWidth = right - left; mThisHeight = bottom - top; Runnable r = mOnLayoutRunnable; if ( r != null ) { mOnLayoutRunnable = null; r.run(); } if ( getDrawable() != null ) { if ( mFitToScreen ) getProperBaseMatrix2( getDrawable(), mBaseMatrix ); else getProperBaseMatrix( getDrawable(), mBaseMatrix ); setImageMatrix( getImageViewMatrix() ); if (mFitToWidth) zoomToWidth(); } } @Override public void setImageBitmap( Bitmap bm ) { setImageBitmap( bm, true ); } @Override public void setImageResource( int resId ) { setImageDrawable( getContext().getResources().getDrawable( resId ) ); } /** * Set the new image to display and reset the internal matrix. * * @param bitmap * - the {@link Bitmap} to display * @param reset * - if true the image bounds will be recreated, otherwise the old {@link Matrix} is used to display the new bitmap * @see #setImageBitmap(Bitmap) */ public void setImageBitmap( final Bitmap bitmap, final boolean reset ) { setImageBitmap( bitmap, reset, null ); } /** * Similar to {@link #setImageBitmap(Bitmap, boolean)} but an optional view {@link Matrix} can be passed to determine the new * bitmap view matrix.
* This method is useful if you need to restore a Bitmap with the same zoom/pan values from a previous state * * @param bitmap * - the {@link Bitmap} to display * @param reset * @param matrix * - the {@link Matrix} to be used to display the new bitmap * * @see #setImageBitmap(Bitmap, boolean) * @see #setImageBitmap(Bitmap) * @see #getImageViewMatrix() * @see #getDisplayMatrix() */ public void setImageBitmap( final Bitmap bitmap, final boolean reset, Matrix matrix ) { setImageBitmap( bitmap, reset, matrix, -1 ); } /** * * @param bitmap * @param reset * @param matrix * @param maxZoom * - maximum zoom allowd during zoom gestures * * @see #setImageBitmap(Bitmap, boolean, Matrix) */ public void setImageBitmap( final Bitmap bitmap, final boolean reset, Matrix matrix, float maxZoom ) { Log.i( LOG_TAG, "setImageBitmap: " + bitmap ); if ( bitmap != null ) setImageDrawable( new FastBitmapDrawable( bitmap ), reset, matrix, maxZoom ); else setImageDrawable( null, reset, matrix, maxZoom ); } @Override public void setImageDrawable( Drawable drawable ) { setImageDrawable( drawable, true, null, -1 ); } public void setImageDrawable( final Drawable drawable, final boolean reset, final Matrix initial_matrix, final float maxZoom ) { final int viewWidth = getWidth(); if ( viewWidth <= 0 ) { mOnLayoutRunnable = new Runnable() { @Override public void run() { setImageDrawable( drawable, reset, initial_matrix, maxZoom ); } }; return; } _setImageDrawable( drawable, reset, initial_matrix, maxZoom ); } protected void _setImageDrawable( final Drawable drawable, final boolean reset, final Matrix initial_matrix, final float maxZoom ) { if ( drawable != null ) { if ( mFitToScreen ) getProperBaseMatrix2( drawable, mBaseMatrix ); else getProperBaseMatrix( drawable, mBaseMatrix ); super.setImageDrawable( drawable ); if (mFitToWidth) zoomToWidth(); } else { mBaseMatrix.reset(); super.setImageDrawable( null ); } if ( reset ) { mSuppMatrix.reset(); if ( initial_matrix != null ) { mSuppMatrix = new Matrix( initial_matrix ); } } setImageMatrix( getImageViewMatrix() ); if ( maxZoom < 1 ) mMaxZoom = maxZoom(); else mMaxZoom = maxZoom; onBitmapChanged( drawable ); } protected void onBitmapChanged( final Drawable bitmap ) { if ( mListener != null ) { mListener.onBitmapChanged( bitmap ); } } protected float maxZoom() { final Drawable drawable = getDrawable(); if ( drawable == null ) { return 1F; } float fw = (float) drawable.getIntrinsicWidth() / (float) mThisWidth; float fh = (float) drawable.getIntrinsicHeight() / (float) mThisHeight; float max = Math.max( fw, fh ) * 4; return max; } public float getMaxZoom() { return mMaxZoom; } public Matrix getImageViewMatrix() { mDisplayMatrix.set( mBaseMatrix ); mDisplayMatrix.postConcat( mSuppMatrix ); return mDisplayMatrix; } /** * Returns the current image display matrix. This matrix can be used in the next call to the * {@link #setImageBitmap(Bitmap, boolean, Matrix)} to restore the same view state of the previous {@link Bitmap} * * @return */ public Matrix getDisplayMatrix() { return new Matrix( mSuppMatrix ); } /** * Setup the base matrix so that the image is centered and scaled properly. * * @param bitmap * @param matrix */ protected void getProperBaseMatrix( Drawable drawable, Matrix matrix ) { float viewWidth = getWidth(); float viewHeight = getHeight(); float w = drawable.getIntrinsicWidth(); float h = drawable.getIntrinsicHeight(); matrix.reset(); if ( w > viewWidth || h > viewHeight ) { float widthScale = Math.min( viewWidth / w, 2.0f ); float heightScale = Math.min( viewHeight / h, 2.0f ); 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 { float tw = ( viewWidth - w ) / 2.0f; float th = ( viewHeight - h ) / 2.0f; matrix.postTranslate( tw, th ); } } /** * 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 = getWidth(); float viewHeight = getHeight(); float w = bitmap.getIntrinsicWidth(); float h = bitmap.getIntrinsicHeight(); matrix.reset(); float widthScale = Math.min( viewWidth / w, MAX_ZOOM ); float heightScale = Math.min( viewHeight / h, MAX_ZOOM ); 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]; } protected RectF getBitmapRect() { final Drawable drawable = getDrawable(); if ( drawable == null ) return null; Matrix m = getImageViewMatrix(); mBitmapRect.set( 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() ); m.mapRect( mBitmapRect ); return mBitmapRect; } protected float getScale( Matrix matrix ) { return getValue( matrix, Matrix.MSCALE_X ); } public float getRotation() { return 0; } public float getScale() { return getScale( mSuppMatrix ); } protected void center( boolean horizontal, boolean vertical ) { // Log.i(LOG_TAG, "center"); final Drawable drawable = getDrawable(); if ( drawable == null ) return; RectF rect = getCenter( horizontal, vertical ); if ( rect.left != 0 || rect.top != 0 ) { postTranslate( rect.left, rect.top ); } } protected RectF getCenter( boolean horizontal, boolean vertical ) { final Drawable drawable = getDrawable(); if ( drawable == null ) return new RectF( 0, 0, 0, 0 ); RectF rect = getBitmapRect(); float height = rect.height(); float width = rect.width(); float deltaX = 0, deltaY = 0; if ( vertical ) { int viewHeight = getHeight(); if ( height < viewHeight ) { deltaY = ( viewHeight - height ) / 2 - rect.top; } else if ( rect.top > 0 ) { deltaY = -rect.top; } else if ( rect.bottom < viewHeight ) { deltaY = getHeight() - rect.bottom; } } if ( horizontal ) { int viewWidth = getWidth(); 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 ) { mSuppMatrix.postTranslate( deltaX, deltaY ); setImageMatrix( getImageViewMatrix() ); } protected void postScale( float scale, float centerX, float centerY ) { mSuppMatrix.postScale( scale, scale, centerX, centerY ); setImageMatrix( getImageViewMatrix() ); } protected void zoomTo( float scale ) { float cx = getWidth() / 2F; float cy = getHeight() / 2F; zoomTo( scale, cx, cy ); } public void zoomTo( float scale, float durationMs ) { float cx = getWidth() / 2F; float cy = getHeight() / 2F; zoomTo( scale, cx, cy, durationMs ); } protected void zoomTo( float scale, float centerX, float centerY ) { if ( scale > mMaxZoom ) scale = mMaxZoom; float oldScale = getScale(); float deltaScale = scale / oldScale; postScale( deltaScale, centerX, centerY ); onZoom( getScale() ); center( true, true ); } protected void onZoom( float scale ) {} 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 ) { float width = getWidth(); float height = getHeight(); if ( bitmapRect.top >= 0 && bitmapRect.bottom <= height ) scrollRect.top = 0; if ( bitmapRect.left >= 0 && bitmapRect.right <= width ) scrollRect.left = 0; if ( bitmapRect.top + scrollRect.top >= 0 && bitmapRect.bottom > height ) scrollRect.top = (int) ( 0 - bitmapRect.top ); if ( bitmapRect.bottom + scrollRect.top <= ( height - 0 ) && bitmapRect.top < 0 ) scrollRect.top = (int) ( ( height - 0 ) - bitmapRect.bottom ); if ( bitmapRect.left + scrollRect.left >= 0 ) scrollRect.left = (int) ( 0 - bitmapRect.left ); if ( bitmapRect.right + scrollRect.left <= ( width - 0 ) ) scrollRect.left = (int) ( ( width - 0 ) - bitmapRect.right ); // Log.d( LOG_TAG, "scrollRect(2): " + scrollRect.toString() ); } 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( true, true ); if ( centerRect.left != 0 || centerRect.top != 0 ) scrollBy( centerRect.left, centerRect.top ); } } } ); } protected void zoomTo( float scale, final float centerX, final float centerY, final float durationMs ) { // Log.i( LOG_TAG, "zoomTo: " + scale + ", " + centerX + ": " + centerY ); final long startTime = System.currentTimeMillis(); final float incrementPerMs = ( scale - getScale() ) / durationMs; final float oldScale = getScale(); mHandler.post( new Runnable() { @Override public void run() { long now = System.currentTimeMillis(); float currentMs = Math.min( durationMs, now - startTime ); float target = oldScale + ( incrementPerMs * currentMs ); zoomTo( target, centerX, centerY ); if ( currentMs < durationMs ) { mHandler.post( this ); } else { // if ( getScale() < 1f ) {} } } } ); } @Override public void dispose() { clear(); } protected void zoomToWidth() { RectF bitmapRect = getBitmapRect(); float w = bitmapRect.right - bitmapRect.left; if (w < getWidth()) { float scale = (float)getWidth() / w; zoomTo(scale, 0f, 0f); } } }