2  *   @author Michael Ortiz 
   3  *   @updated Patrick Lackemacher 
   8  *   Copyright (c) 2012 Michael Ortiz 
  11 package third_parties
.michaelOrtiz
; 
  13 import com
.owncloud
.android
.ui
.preview
.ImageViewCustom
; 
  15 import android
.annotation
.TargetApi
; 
  16 import android
.content
.Context
; 
  17 import android
.content
.res
.Configuration
; 
  18 import android
.graphics
.Bitmap
; 
  19 import android
.graphics
.Canvas
; 
  20 import android
.graphics
.Matrix
; 
  21 import android
.graphics
.PointF
; 
  22 import android
.graphics
.RectF
; 
  23 import android
.graphics
.drawable
.Drawable
; 
  24 import android
.net
.Uri
; 
  25 import android
.os
.Build
; 
  26 import android
.os
.Build
.VERSION
; 
  27 import android
.os
.Build
.VERSION_CODES
; 
  28 import android
.os
.Bundle
; 
  29 import android
.os
.Parcelable
; 
  30 import android
.util
.AttributeSet
; 
  31 import android
.util
.Log
; 
  32 import android
.view
.GestureDetector
; 
  33 import android
.view
.MotionEvent
; 
  34 import android
.view
.ScaleGestureDetector
; 
  35 import android
.view
.View
; 
  36 import android
.view
.animation
.AccelerateDecelerateInterpolator
; 
  37 import android
.widget
.OverScroller
; 
  38 import android
.widget
.Scroller
; 
  41  * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom. 
  43 public class TouchImageViewCustom 
extends ImageViewCustom 
{ 
  44     private static final String DEBUG 
= "DEBUG"; 
  47         // SuperMin and SuperMax multipliers. Determine how much the image can be 
  48         // zoomed below or above the zoom boundaries, before animating back to the 
  49         // min/max zoom boundary. 
  51         private static final float SUPER_MIN_MULTIPLIER 
= .75f
; 
  52         private static final float SUPER_MAX_MULTIPLIER 
= 1.25f
; 
  55     // Scale of image ranges from minScale to maxScale, where minScale == 1 
  56     // when the image is stretched to fit view. 
  58     private float normalizedScale
; 
  61     // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal. 
  62     // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix 
  63     // saved prior to the screen rotating. 
  65         private Matrix matrix
, prevMatrix
; 
  67     private static enum State 
{ NONE
, DRAG
, ZOOM
, FLING
, ANIMATE_ZOOM 
}; 
  70     private float minScale
; 
  71     private float maxScale
; 
  72     private float superMinScale
; 
  73     private float superMaxScale
; 
  76     private Context context
; 
  79     private ScaleType mScaleType
; 
  81     private boolean imageRenderedAtLeastOnce
; 
  82     private boolean onDrawReady
; 
  84     private ZoomVariables delayedZoomVariables
; 
  87     // Size of view and previous view size (ie before rotation) 
  89     private int viewWidth
, viewHeight
, prevViewWidth
, prevViewHeight
; 
  92     // Size of image when it is stretched to fit view. Before and After rotation. 
  94     private float matchViewWidth
, matchViewHeight
, prevMatchViewWidth
, prevMatchViewHeight
; 
  96     private ScaleGestureDetector mScaleDetector
; 
  97     private GestureDetector mGestureDetector
; 
  98     private GestureDetector
.OnDoubleTapListener doubleTapListener 
= null
; 
  99     private OnTouchListener userTouchListener 
= null
; 
 100     private OnTouchImageViewListener touchImageViewListener 
= null
; 
 102     public TouchImageViewCustom(Context context
) { 
 104         sharedConstructing(context
); 
 107     public TouchImageViewCustom(Context context
, AttributeSet attrs
) { 
 108         super(context
, attrs
); 
 109         sharedConstructing(context
); 
 112     public TouchImageViewCustom(Context context
, AttributeSet attrs
, int defStyle
) { 
 113         super(context
, attrs
, defStyle
); 
 114         sharedConstructing(context
); 
 117     private void sharedConstructing(Context context
) { 
 118         super.setClickable(true
); 
 119         this.context 
= context
; 
 120         mScaleDetector 
= new ScaleGestureDetector(context
, new ScaleListener()); 
 121         mGestureDetector 
= new GestureDetector(context
, new GestureListener()); 
 122         matrix 
= new Matrix(); 
 123         prevMatrix 
= new Matrix(); 
 126         if (mScaleType 
== null
) { 
 127                 mScaleType 
= ScaleType
.FIT_CENTER
; 
 131         superMinScale 
= SUPER_MIN_MULTIPLIER 
* minScale
; 
 132         superMaxScale 
= SUPER_MAX_MULTIPLIER 
* maxScale
; 
 133         setImageMatrix(matrix
); 
 134         setScaleType(ScaleType
.MATRIX
); 
 135         setState(State
.NONE
); 
 137         super.setOnTouchListener(new PrivateOnTouchListener()); 
 141     public void setOnTouchListener(View
.OnTouchListener l
) { 
 142         userTouchListener 
= l
; 
 145     public void setOnTouchImageViewListener(OnTouchImageViewListener l
) { 
 146         touchImageViewListener 
= l
; 
 149     public void setOnDoubleTapListener(GestureDetector
.OnDoubleTapListener l
) { 
 150         doubleTapListener 
= l
; 
 154     public void setImageResource(int resId
) { 
 155         super.setImageResource(resId
); 
 156         savePreviousImageValues(); 
 161     public void setImageBitmap(Bitmap bm
) { 
 162         super.setImageBitmap(bm
); 
 163         savePreviousImageValues(); 
 168     public void setImageDrawable(Drawable drawable
) { 
 169         super.setImageDrawable(drawable
); 
 170         savePreviousImageValues(); 
 175     public void setImageURI(Uri uri
) { 
 176         super.setImageURI(uri
); 
 177         savePreviousImageValues(); 
 182     public void setScaleType(ScaleType type
) { 
 183         if (type 
== ScaleType
.FIT_START 
|| type 
== ScaleType
.FIT_END
) { 
 184                 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); 
 186         if (type 
== ScaleType
.MATRIX
) { 
 187                 super.setScaleType(ScaleType
.MATRIX
); 
 193                         // If the image is already rendered, scaleType has been called programmatically 
 194                         // and the TouchImageView should be updated with the new scaleType. 
 202     public ScaleType 
getScaleType() { 
 207      * Returns false if image is in initial, unzoomed state. False, otherwise. 
 208      * @return true if image is zoomed 
 210     public boolean isZoomed() { 
 211         return normalizedScale 
!= 1; 
 215      * Return a Rect representing the zoomed image. 
 216      * @return rect representing zoomed image 
 218     public RectF 
getZoomedRect() { 
 219         if (mScaleType 
== ScaleType
.FIT_XY
) { 
 220                 throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY"); 
 222         PointF topLeft 
= transformCoordTouchToBitmap(0, 0, true
); 
 223         PointF bottomRight 
= transformCoordTouchToBitmap(viewWidth
, viewHeight
, true
); 
 225         float w 
= getDrawable().getIntrinsicWidth(); 
 226         float h 
= getDrawable().getIntrinsicHeight(); 
 227         return new RectF(topLeft
.x 
/ w
, topLeft
.y 
/ h
, bottomRight
.x 
/ w
, bottomRight
.y 
/ h
); 
 231      * Save the current matrix and view dimensions 
 232      * in the prevMatrix and prevView variables. 
 234     private void savePreviousImageValues() { 
 235         if (matrix 
!= null 
&& viewHeight 
!= 0 && viewWidth 
!= 0) { 
 237                 prevMatrix
.setValues(m
); 
 238                 prevMatchViewHeight 
= matchViewHeight
; 
 239                 prevMatchViewWidth 
= matchViewWidth
; 
 240                 prevViewHeight 
= viewHeight
; 
 241                 prevViewWidth 
= viewWidth
; 
 246     public Parcelable 
onSaveInstanceState() { 
 247         Bundle bundle 
= new Bundle(); 
 248         bundle
.putParcelable("instanceState", super.onSaveInstanceState()); 
 249         bundle
.putFloat("saveScale", normalizedScale
); 
 250         bundle
.putFloat("matchViewHeight", matchViewHeight
); 
 251         bundle
.putFloat("matchViewWidth", matchViewWidth
); 
 252         bundle
.putInt("viewWidth", viewWidth
); 
 253         bundle
.putInt("viewHeight", viewHeight
); 
 255         bundle
.putFloatArray("matrix", m
); 
 256         bundle
.putBoolean("imageRendered", imageRenderedAtLeastOnce
); 
 261     public void onRestoreInstanceState(Parcelable state
) { 
 262         if (state 
instanceof Bundle
) { 
 263                 Bundle bundle 
= (Bundle
) state
; 
 264                 normalizedScale 
= bundle
.getFloat("saveScale"); 
 265                 m 
= bundle
.getFloatArray("matrix"); 
 266                 prevMatrix
.setValues(m
); 
 267                 prevMatchViewHeight 
= bundle
.getFloat("matchViewHeight"); 
 268                 prevMatchViewWidth 
= bundle
.getFloat("matchViewWidth"); 
 269                 prevViewHeight 
= bundle
.getInt("viewHeight"); 
 270                 prevViewWidth 
= bundle
.getInt("viewWidth"); 
 271                 imageRenderedAtLeastOnce 
= bundle
.getBoolean("imageRendered"); 
 272                 super.onRestoreInstanceState(bundle
.getParcelable("instanceState")); 
 276         super.onRestoreInstanceState(state
); 
 280     protected void onDraw(Canvas canvas
) { 
 282         imageRenderedAtLeastOnce 
= true
; 
 283         if (delayedZoomVariables 
!= null
) { 
 284                 setZoom(delayedZoomVariables
.scale
, delayedZoomVariables
.focusX
, delayedZoomVariables
.focusY
, delayedZoomVariables
.scaleType
); 
 285                 delayedZoomVariables 
= null
; 
 287         super.onDraw(canvas
); 
 291     public void onConfigurationChanged(Configuration newConfig
) { 
 292         super.onConfigurationChanged(newConfig
); 
 293         savePreviousImageValues(); 
 297      * Get the max zoom multiplier. 
 298      * @return max zoom multiplier. 
 300     public float getMaxZoom() { 
 305      * Set the max zoom multiplier. Default value: 3. 
 306      * @param max max zoom multiplier. 
 308     public void setMaxZoom(float max
) { 
 310         superMaxScale 
= SUPER_MAX_MULTIPLIER 
* maxScale
; 
 314      * Get the min zoom multiplier. 
 315      * @return min zoom multiplier. 
 317     public float getMinZoom() { 
 322      * Get the current zoom. This is the zoom relative to the initial 
 323      * scale, not the original resource. 
 324      * @return current zoom multiplier. 
 326     public float getCurrentZoom() { 
 327         return normalizedScale
; 
 331      * Set the min zoom multiplier. Default value: 1. 
 332      * @param min min zoom multiplier. 
 334     public void setMinZoom(float min
) { 
 336         superMinScale 
= SUPER_MIN_MULTIPLIER 
* minScale
; 
 340      * Reset zoom and translation to initial state. 
 342     public void resetZoom() { 
 348      * Set zoom to the specified scale. Image will be centered by default. 
 351     public void setZoom(float scale
) { 
 352         setZoom(scale
, 0.5f
, 0.5f
); 
 356      * Set zoom to the specified scale. Image will be centered around the point 
 357      * (focusX, focusY). These floats range from 0 to 1 and denote the focus point 
 358      * as a fraction from the left and top of the view. For example, the top left  
 359      * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 
 364     public void setZoom(float scale
, float focusX
, float focusY
) { 
 365         setZoom(scale
, focusX
, focusY
, mScaleType
); 
 369      * Set zoom to the specified scale. Image will be centered around the point 
 370      * (focusX, focusY). These floats range from 0 to 1 and denote the focus point 
 371      * as a fraction from the left and top of the view. For example, the top left  
 372      * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 
 378     public void setZoom(float scale
, float focusX
, float focusY
, ScaleType scaleType
) { 
 380         // setZoom can be called before the image is on the screen, but at this point,  
 381         // image and view sizes have not yet been calculated in onMeasure. Thus, we should 
 382         // delay calling setZoom until the view has been measured. 
 385                 delayedZoomVariables 
= new ZoomVariables(scale
, focusX
, focusY
, scaleType
); 
 389         if (scaleType 
!= mScaleType
) { 
 390                 setScaleType(scaleType
); 
 393         scaleImage(scale
, viewWidth 
/ 2, viewHeight 
/ 2, true
); 
 395         m
[Matrix
.MTRANS_X
] = -((focusX 
* getImageWidth()) - (viewWidth 
* 0.5f
)); 
 396         m
[Matrix
.MTRANS_Y
] = -((focusY 
* getImageHeight()) - (viewHeight 
* 0.5f
)); 
 399         setImageMatrix(matrix
); 
 403      * Set zoom parameters equal to another TouchImageView. Including scale, position, 
 407     public void setZoom(TouchImageViewCustom img
) { 
 408         PointF center 
= img
.getScrollPosition(); 
 409         setZoom(img
.getCurrentZoom(), center
.x
, center
.y
, img
.getScaleType()); 
 413      * Return the point at the center of the zoomed image. The PointF coordinates range 
 414      * in value between 0 and 1 and the focus point is denoted as a fraction from the left  
 415      * and top of the view. For example, the top left corner of the image would be (0, 0).  
 416      * And the bottom right corner would be (1, 1). 
 417      * @return PointF representing the scroll position of the zoomed image. 
 419     public PointF 
getScrollPosition() { 
 420         Drawable drawable 
= getDrawable(); 
 421         if (drawable 
== null
) { 
 424         int drawableWidth 
= drawable
.getIntrinsicWidth(); 
 425         int drawableHeight 
= drawable
.getIntrinsicHeight(); 
 427         PointF point 
= transformCoordTouchToBitmap(viewWidth 
/ 2, viewHeight 
/ 2, true
); 
 428         point
.x 
/= drawableWidth
; 
 429         point
.y 
/= drawableHeight
; 
 434      * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the 
 435      * left and top of the view. The focus points can range in value between 0 and 1.  
 439     public void setScrollPosition(float focusX
, float focusY
) { 
 440         setZoom(normalizedScale
, focusX
, focusY
); 
 444      * Performs boundary checking and fixes the image matrix if it  
 447     private void fixTrans() { 
 449         float transX 
= m
[Matrix
.MTRANS_X
]; 
 450         float transY 
= m
[Matrix
.MTRANS_Y
]; 
 452         float fixTransX 
= getFixTrans(transX
, viewWidth
, getImageWidth()); 
 453         float fixTransY 
= getFixTrans(transY
, viewHeight
, getImageHeight()); 
 455         if (fixTransX 
!= 0 || fixTransY 
!= 0) { 
 456             matrix
.postTranslate(fixTransX
, fixTransY
); 
 461      * When transitioning from zooming from focus to zoom from center (or vice versa) 
 462      * the image can become unaligned within the view. This is apparent when zooming 
 463      * quickly. When the content size is less than the view size, the content will often 
 464      * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and  
 465      * then makes sure the image is centered correctly within the view. 
 467     private void fixScaleTrans() { 
 470         if (getImageWidth() < viewWidth
) { 
 471                 m
[Matrix
.MTRANS_X
] = (viewWidth 
- getImageWidth()) / 2; 
 474         if (getImageHeight() < viewHeight
) { 
 475                 m
[Matrix
.MTRANS_Y
] = (viewHeight 
- getImageHeight()) / 2; 
 480     private float getFixTrans(float trans
, float viewSize
, float contentSize
) { 
 481         float minTrans
, maxTrans
; 
 483         if (contentSize 
<= viewSize
) { 
 485             maxTrans 
= viewSize 
- contentSize
; 
 488             minTrans 
= viewSize 
- contentSize
; 
 492         if (trans 
< minTrans
) 
 493             return -trans 
+ minTrans
; 
 494         if (trans 
> maxTrans
) 
 495             return -trans 
+ maxTrans
; 
 499     private float getFixDragTrans(float delta
, float viewSize
, float contentSize
) { 
 500         if (contentSize 
<= viewSize
) { 
 506     private float getImageWidth() { 
 507         return matchViewWidth 
* normalizedScale
; 
 510     private float getImageHeight() { 
 511         return matchViewHeight 
* normalizedScale
; 
 515     protected void onMeasure(int widthMeasureSpec
, int heightMeasureSpec
) { 
 516         Drawable drawable 
= getDrawable(); 
 517         if (drawable 
== null 
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) { 
 518                 setMeasuredDimension(0, 0); 
 522         int drawableWidth 
= drawable
.getIntrinsicWidth(); 
 523         int drawableHeight 
= drawable
.getIntrinsicHeight(); 
 524         int widthSize 
= MeasureSpec
.getSize(widthMeasureSpec
); 
 525         int widthMode 
= MeasureSpec
.getMode(widthMeasureSpec
); 
 526         int heightSize 
= MeasureSpec
.getSize(heightMeasureSpec
); 
 527         int heightMode 
= MeasureSpec
.getMode(heightMeasureSpec
); 
 528         viewWidth 
= setViewSize(widthMode
, widthSize
, drawableWidth
); 
 529         viewHeight 
= setViewSize(heightMode
, heightSize
, drawableHeight
); 
 532         // Set view dimensions 
 534         setMeasuredDimension(viewWidth
, viewHeight
); 
 537         // Fit content within view 
 543      * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise, 
 544      * it is made to fit the screen according to the dimensions of the previous image matrix. This 
 545      * allows the image to maintain its zoom after rotation. 
 547     private void fitImageToView() { 
 548         Drawable drawable 
= getDrawable(); 
 549         if (drawable 
== null 
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) { 
 552         if (matrix 
== null 
|| prevMatrix 
== null
) { 
 556         int drawableWidth 
= drawable
.getIntrinsicWidth(); 
 557         int drawableHeight 
= drawable
.getIntrinsicHeight(); 
 560         // Scale image for view 
 562         float scaleX 
= (float) viewWidth 
/ drawableWidth
; 
 563         float scaleY 
= (float) viewHeight 
/ drawableHeight
; 
 565         switch (mScaleType
) { 
 571                 scaleX 
= scaleY 
= Math
.max(scaleX
, scaleY
); 
 575                 scaleX 
= scaleY 
= Math
.min(1, Math
.min(scaleX
, scaleY
)); 
 578                 scaleX 
= scaleY 
= Math
.min(scaleX
, scaleY
); 
 586                 // FIT_START and FIT_END not supported 
 588                 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); 
 595         float redundantXSpace 
= viewWidth 
- (scaleX 
* drawableWidth
); 
 596         float redundantYSpace 
= viewHeight 
- (scaleY 
* drawableHeight
); 
 597         matchViewWidth 
= viewWidth 
- redundantXSpace
; 
 598         matchViewHeight 
= viewHeight 
- redundantYSpace
; 
 599         if (!isZoomed() && !imageRenderedAtLeastOnce
) { 
 601                 // Stretch and center image to fit view 
 603                 matrix
.setScale(scaleX
, scaleY
); 
 604                 matrix
.postTranslate(redundantXSpace 
/ 2, redundantYSpace 
/ 2); 
 609                 // These values should never be 0 or we will set viewWidth and viewHeight 
 610                 // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues 
 611                 // to set them equal to the current values. 
 613                 if (prevMatchViewWidth 
== 0 || prevMatchViewHeight 
== 0) { 
 614                         savePreviousImageValues(); 
 617                 prevMatrix
.getValues(m
); 
 620                 // Rescale Matrix after rotation 
 622                 m
[Matrix
.MSCALE_X
] = matchViewWidth 
/ drawableWidth 
* normalizedScale
; 
 623                 m
[Matrix
.MSCALE_Y
] = matchViewHeight 
/ drawableHeight 
* normalizedScale
; 
 626                 // TransX and TransY from previous matrix 
 628             float transX 
= m
[Matrix
.MTRANS_X
]; 
 629             float transY 
= m
[Matrix
.MTRANS_Y
]; 
 634             float prevActualWidth 
= prevMatchViewWidth 
* normalizedScale
; 
 635             float actualWidth 
= getImageWidth(); 
 636             translateMatrixAfterRotate(Matrix
.MTRANS_X
, transX
, prevActualWidth
, actualWidth
, prevViewWidth
, viewWidth
, drawableWidth
); 
 641             float prevActualHeight 
= prevMatchViewHeight 
* normalizedScale
; 
 642             float actualHeight 
= getImageHeight(); 
 643             translateMatrixAfterRotate(Matrix
.MTRANS_Y
, transY
, prevActualHeight
, actualHeight
, prevViewHeight
, viewHeight
, drawableHeight
); 
 646             // Set the matrix to the adjusted scale and translate values. 
 651         setImageMatrix(matrix
); 
 655      * Set view dimensions based on layout params 
 659      * @param drawableWidth 
 662     private int setViewSize(int mode
, int size
, int drawableWidth
) { 
 665                 case MeasureSpec
.EXACTLY
: 
 669                 case MeasureSpec
.AT_MOST
: 
 670                         viewSize 
= Math
.min(drawableWidth
, size
); 
 673                 case MeasureSpec
.UNSPECIFIED
: 
 674                         viewSize 
= drawableWidth
; 
 685      * After rotating, the matrix needs to be translated. This function finds the area of image  
 686      * which was previously centered and adjusts translations so that is again the center, post-rotation. 
 688      * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y 
 689      * @param trans the value of trans in that axis before the rotation 
 690      * @param prevImageSize the width/height of the image before the rotation 
 691      * @param imageSize width/height of the image after rotation 
 692      * @param prevViewSize width/height of view before rotation 
 693      * @param viewSize width/height of view after rotation 
 694      * @param drawableSize width/height of drawable 
 696     private void translateMatrixAfterRotate(int axis
, float trans
, float prevImageSize
, float imageSize
, int prevViewSize
, int viewSize
, int drawableSize
) { 
 697         if (imageSize 
< viewSize
) { 
 699                 // The width/height of image is less than the view's width/height. Center it. 
 701                 m
[axis
] = (viewSize 
- (drawableSize 
* m
[Matrix
.MSCALE_X
])) * 0.5f
; 
 703         } else if (trans 
> 0) { 
 705                 // The image is larger than the view, but was not before rotation. Center it. 
 707                 m
[axis
] = -((imageSize 
- viewSize
) * 0.5f
); 
 711                 // Find the area of the image which was previously centered in the view. Determine its distance 
 712                 // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage 
 713                 // to calculate the trans in the new view width/height. 
 715                 float percentage 
= (Math
.abs(trans
) + (0.5f 
* prevViewSize
)) / prevImageSize
; 
 716                 m
[axis
] = -((percentage 
* imageSize
) - (viewSize 
* 0.5f
)); 
 720     private void setState(State state
) { 
 724     public boolean canScrollHorizontallyFroyo(int direction
) { 
 725         return canScrollHorizontally(direction
); 
 729     public boolean canScrollHorizontally(int direction
) { 
 731         float x 
= m
[Matrix
.MTRANS_X
]; 
 733         if (getImageWidth() < viewWidth
) { 
 736         } else if (x 
>= -1 && direction 
< 0) { 
 739         } else if (Math
.abs(x
) + viewWidth 
+ 1 >= getImageWidth() && direction 
> 0) { 
 747      * Gesture Listener detects a single click or long click and passes that on 
 748      * to the view's listener. 
 752     private class GestureListener 
extends GestureDetector
.SimpleOnGestureListener 
{ 
 755         public boolean onSingleTapConfirmed(MotionEvent e
) 
 757             if(doubleTapListener 
!= null
) { 
 758                 return doubleTapListener
.onSingleTapConfirmed(e
); 
 760                 return performClick(); 
 764         public void onLongPress(MotionEvent e
) 
 770         public boolean onFling(MotionEvent e1
, MotionEvent e2
, float velocityX
, float velocityY
) 
 774                         // If a previous fling is still active, it should be cancelled so that two flings 
 775                         // are not run simultaenously. 
 779                 fling 
= new Fling((int) velocityX
, (int) velocityY
); 
 780                 compatPostOnAnimation(fling
); 
 781                 return super.onFling(e1
, e2
, velocityX
, velocityY
); 
 785         public boolean onDoubleTap(MotionEvent e
) { 
 786                 boolean consumed 
= false
; 
 787             if(doubleTapListener 
!= null
) { 
 788                 consumed 
= doubleTapListener
.onDoubleTap(e
); 
 790                 if (state 
== State
.NONE
) { 
 791                         float targetZoom 
= (normalizedScale 
== minScale
) ? maxScale 
: minScale
; 
 792                         DoubleTapZoom doubleTap 
= new DoubleTapZoom(targetZoom
, e
.getX(), e
.getY(), false
); 
 793                         compatPostOnAnimation(doubleTap
); 
 800         public boolean onDoubleTapEvent(MotionEvent e
) { 
 801             if(doubleTapListener 
!= null
) { 
 802                 return doubleTapListener
.onDoubleTapEvent(e
); 
 808     public interface OnTouchImageViewListener 
{ 
 809         public void onMove(); 
 813      * Responsible for all touch events. Handles the heavy lifting of drag and also sends 
 814      * touch events to Scale Detector and Gesture Detector. 
 818     private class PrivateOnTouchListener 
implements OnTouchListener 
{ 
 821         // Remember last point position for dragging 
 823         private PointF last 
= new PointF(); 
 826         public boolean onTouch(View v
, MotionEvent event
) { 
 827             mScaleDetector
.onTouchEvent(event
); 
 828             mGestureDetector
.onTouchEvent(event
); 
 829             PointF curr 
= new PointF(event
.getX(), event
.getY()); 
 831             if (state 
== State
.NONE 
|| state 
== State
.DRAG 
|| state 
== State
.FLING
) { 
 832                     switch (event
.getAction()) { 
 833                         case MotionEvent
.ACTION_DOWN
: 
 837                             setState(State
.DRAG
); 
 840                         case MotionEvent
.ACTION_MOVE
: 
 841                             if (state 
== State
.DRAG
) { 
 842                                 float deltaX 
= curr
.x 
- last
.x
; 
 843                                 float deltaY 
= curr
.y 
- last
.y
; 
 844                                 float fixTransX 
= getFixDragTrans(deltaX
, viewWidth
, getImageWidth()); 
 845                                 float fixTransY 
= getFixDragTrans(deltaY
, viewHeight
, getImageHeight()); 
 846                                 matrix
.postTranslate(fixTransX
, fixTransY
); 
 848                                 last
.set(curr
.x
, curr
.y
); 
 852                         case MotionEvent
.ACTION_UP
: 
 853                         case MotionEvent
.ACTION_POINTER_UP
: 
 854                             setState(State
.NONE
); 
 859             setImageMatrix(matrix
); 
 862                 // User-defined OnTouchListener 
 864                 if(userTouchListener 
!= null
) { 
 865                         userTouchListener
.onTouch(v
, event
); 
 869                 // OnTouchImageViewListener is set: TouchImageView dragged by user. 
 871                 if (touchImageViewListener 
!= null
) { 
 872                         touchImageViewListener
.onMove(); 
 876             // indicate event was handled 
 883      * ScaleListener detects user two finger scaling and scales image. 
 887     private class ScaleListener 
extends ScaleGestureDetector
.SimpleOnScaleGestureListener 
{ 
 889         public boolean onScaleBegin(ScaleGestureDetector detector
) { 
 890             setState(State
.ZOOM
); 
 895         public boolean onScale(ScaleGestureDetector detector
) { 
 896                 scaleImage(detector
.getScaleFactor(), detector
.getFocusX(), detector
.getFocusY(), true
); 
 899                 // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user. 
 901                 if (touchImageViewListener 
!= null
) { 
 902                         touchImageViewListener
.onMove(); 
 908         public void onScaleEnd(ScaleGestureDetector detector
) { 
 909                 super.onScaleEnd(detector
); 
 910                 setState(State
.NONE
); 
 911                 boolean animateToZoomBoundary 
= false
; 
 912                 float targetZoom 
= normalizedScale
; 
 913                 if (normalizedScale 
> maxScale
) { 
 914                         targetZoom 
= maxScale
; 
 915                         animateToZoomBoundary 
= true
; 
 917                 } else if (normalizedScale 
< minScale
) { 
 918                         targetZoom 
= minScale
; 
 919                         animateToZoomBoundary 
= true
; 
 922                 if (animateToZoomBoundary
) { 
 923                         DoubleTapZoom doubleTap 
= new DoubleTapZoom(targetZoom
, viewWidth 
/ 2, viewHeight 
/ 2, true
); 
 924                         compatPostOnAnimation(doubleTap
); 
 929     private void scaleImage(double deltaScale
, float focusX
, float focusY
, boolean stretchImageToSuper
) { 
 931         float lowerScale
, upperScale
; 
 932         if (stretchImageToSuper
) { 
 933                 lowerScale 
= superMinScale
; 
 934                 upperScale 
= superMaxScale
; 
 937                 lowerScale 
= minScale
; 
 938                 upperScale 
= maxScale
; 
 941         float origScale 
= normalizedScale
; 
 942         normalizedScale 
*= deltaScale
; 
 943         if (normalizedScale 
> upperScale
) { 
 944             normalizedScale 
= upperScale
; 
 945             deltaScale 
= upperScale 
/ origScale
; 
 946         } else if (normalizedScale 
< lowerScale
) { 
 947             normalizedScale 
= lowerScale
; 
 948             deltaScale 
= lowerScale 
/ origScale
; 
 951         matrix
.postScale((float) deltaScale
, (float) deltaScale
, focusX
, focusY
); 
 956      * DoubleTapZoom calls a series of runnables which apply 
 957      * an animated zoom in/out graphic to the image. 
 961     private class DoubleTapZoom 
implements Runnable 
{ 
 963         private long startTime
; 
 964         private static final float ZOOM_TIME 
= 500; 
 965         private float startZoom
, targetZoom
; 
 966         private float bitmapX
, bitmapY
; 
 967         private boolean stretchImageToSuper
; 
 968         private AccelerateDecelerateInterpolator interpolator 
= new AccelerateDecelerateInterpolator(); 
 969         private PointF startTouch
; 
 970         private PointF endTouch
; 
 972         DoubleTapZoom(float targetZoom
, float focusX
, float focusY
, boolean stretchImageToSuper
) { 
 973                 setState(State
.ANIMATE_ZOOM
); 
 974                 startTime 
= System
.currentTimeMillis(); 
 975                 this.startZoom 
= normalizedScale
; 
 976                 this.targetZoom 
= targetZoom
; 
 977                 this.stretchImageToSuper 
= stretchImageToSuper
; 
 978                 PointF bitmapPoint 
= transformCoordTouchToBitmap(focusX
, focusY
, false
); 
 979                 this.bitmapX 
= bitmapPoint
.x
; 
 980                 this.bitmapY 
= bitmapPoint
.y
; 
 983                 // Used for translating image during scaling 
 985                 startTouch 
= transformCoordBitmapToTouch(bitmapX
, bitmapY
); 
 986                 endTouch 
= new PointF(viewWidth 
/ 2, viewHeight 
/ 2); 
 991                         float t 
= interpolate(); 
 992                         double deltaScale 
= calculateDeltaScale(t
); 
 993                         scaleImage(deltaScale
, bitmapX
, bitmapY
, stretchImageToSuper
); 
 994                         translateImageToCenterTouchPosition(t
); 
 996                         setImageMatrix(matrix
); 
 999                         // OnTouchImageViewListener is set: double tap runnable updates listener 
1000                         // with every frame. 
1002                         if (touchImageViewListener 
!= null
) { 
1003                                 touchImageViewListener
.onMove(); 
1008                                 // We haven't finished zooming 
1010                                 compatPostOnAnimation(this); 
1016                                 setState(State
.NONE
); 
1021                  * Interpolate between where the image should start and end in order to translate 
1022                  * the image so that the point that is touched is what ends up centered at the end 
1026                 private void translateImageToCenterTouchPosition(float t
) { 
1027                         float targetX 
= startTouch
.x 
+ t 
* (endTouch
.x 
- startTouch
.x
); 
1028                         float targetY 
= startTouch
.y 
+ t 
* (endTouch
.y 
- startTouch
.y
); 
1029                         PointF curr 
= transformCoordBitmapToTouch(bitmapX
, bitmapY
); 
1030                         matrix
.postTranslate(targetX 
- curr
.x
, targetY 
- curr
.y
); 
1034                  * Use interpolator to get t 
1037                 private float interpolate() { 
1038                         long currTime 
= System
.currentTimeMillis(); 
1039                         float elapsed 
= (currTime 
- startTime
) / ZOOM_TIME
; 
1040                         elapsed 
= Math
.min(1f
, elapsed
); 
1041                         return interpolator
.getInterpolation(elapsed
); 
1045                  * Interpolate the current targeted zoom and get the delta 
1046                  * from the current zoom. 
1050                 private double calculateDeltaScale(float t
) { 
1051                         double zoom 
= startZoom 
+ t 
* (targetZoom 
- startZoom
); 
1052                         return zoom 
/ normalizedScale
; 
1057      * This function will transform the coordinates in the touch event to the coordinate  
1058      * system of the drawable that the imageview contain 
1059      * @param x x-coordinate of touch event 
1060      * @param y y-coordinate of touch event 
1061      * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value 
1062      *                  to the bounds of the bitmap size. 
1063      * @return Coordinates of the point touched, in the coordinate system of the original drawable. 
1065     private PointF 
transformCoordTouchToBitmap(float x
, float y
, boolean clipToBitmap
) { 
1066          matrix
.getValues(m
); 
1067          float origW 
= getDrawable().getIntrinsicWidth(); 
1068          float origH 
= getDrawable().getIntrinsicHeight(); 
1069          float transX 
= m
[Matrix
.MTRANS_X
]; 
1070          float transY 
= m
[Matrix
.MTRANS_Y
]; 
1071          float finalX 
= ((x 
- transX
) * origW
) / getImageWidth(); 
1072          float finalY 
= ((y 
- transY
) * origH
) / getImageHeight(); 
1075                  finalX 
= Math
.min(Math
.max(finalX
, 0), origW
); 
1076                  finalY 
= Math
.min(Math
.max(finalY
, 0), origH
); 
1079          return new PointF(finalX 
, finalY
); 
1083      * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the 
1084      * drawable's coordinate system to the view's coordinate system. 
1085      * @param bx x-coordinate in original bitmap coordinate system 
1086      * @param by y-coordinate in original bitmap coordinate system 
1087      * @return Coordinates of the point in the view's coordinate system. 
1089     private PointF 
transformCoordBitmapToTouch(float bx
, float by
) { 
1090         matrix
.getValues(m
);         
1091         float origW 
= getDrawable().getIntrinsicWidth(); 
1092         float origH 
= getDrawable().getIntrinsicHeight(); 
1093         float px 
= bx 
/ origW
; 
1094         float py 
= by 
/ origH
; 
1095         float finalX 
= m
[Matrix
.MTRANS_X
] + getImageWidth() * px
; 
1096         float finalY 
= m
[Matrix
.MTRANS_Y
] + getImageHeight() * py
; 
1097         return new PointF(finalX 
, finalY
); 
1101      * Fling launches sequential runnables which apply 
1102      * the fling graphic to the image. The values for the translation 
1103      * are interpolated by the Scroller. 
1107     private class Fling 
implements Runnable 
{ 
1109         CompatScroller scroller
; 
1112         Fling(int velocityX
, int velocityY
) { 
1113                 setState(State
.FLING
); 
1114                 scroller 
= new CompatScroller(context
); 
1115                 matrix
.getValues(m
); 
1117                 int startX 
= (int) m
[Matrix
.MTRANS_X
]; 
1118                 int startY 
= (int) m
[Matrix
.MTRANS_Y
]; 
1119                 int minX
, maxX
, minY
, maxY
; 
1121                 if (getImageWidth() > viewWidth
) { 
1122                         minX 
= viewWidth 
- (int) getImageWidth(); 
1126                         minX 
= maxX 
= startX
; 
1129                 if (getImageHeight() > viewHeight
) { 
1130                         minY 
= viewHeight 
- (int) getImageHeight(); 
1134                         minY 
= maxY 
= startY
; 
1137                 scroller
.fling(startX
, startY
, (int) velocityX
, (int) velocityY
, minX
, 
1143         public void cancelFling() { 
1144                 if (scroller 
!= null
) { 
1145                         setState(State
.NONE
); 
1146                         scroller
.forceFinished(true
); 
1154                         // OnTouchImageViewListener is set: TouchImageView listener has been flung by user. 
1155                         // Listener runnable updated with each frame of fling animation. 
1157                         if (touchImageViewListener 
!= null
) { 
1158                                 touchImageViewListener
.onMove(); 
1161                         if (scroller
.isFinished()) { 
1166                         if (scroller
.computeScrollOffset()) { 
1167                         int newX 
= scroller
.getCurrX(); 
1168                     int newY 
= scroller
.getCurrY(); 
1169                     int transX 
= newX 
- currX
; 
1170                     int transY 
= newY 
- currY
; 
1173                     matrix
.postTranslate(transX
, transY
); 
1175                     setImageMatrix(matrix
); 
1176                     compatPostOnAnimation(this); 
1181     @TargetApi(Build
.VERSION_CODES
.GINGERBREAD
) 
1182         private class CompatScroller 
{ 
1184         OverScroller overScroller
; 
1185         boolean isPreGingerbread
; 
1187         public CompatScroller(Context context
) { 
1188                 if (VERSION
.SDK_INT 
< VERSION_CODES
.GINGERBREAD
) { 
1189                         isPreGingerbread 
= true
; 
1190                         scroller 
= new Scroller(context
); 
1193                         isPreGingerbread 
= false
; 
1194                         overScroller 
= new OverScroller(context
); 
1198         public void fling(int startX
, int startY
, int velocityX
, int velocityY
, int minX
, int maxX
, int minY
, int maxY
) { 
1199                 if (isPreGingerbread
) { 
1200                         scroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
); 
1202                         overScroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
); 
1206         public void forceFinished(boolean finished
) { 
1207                 if (isPreGingerbread
) { 
1208                         scroller
.forceFinished(finished
); 
1210                         overScroller
.forceFinished(finished
); 
1214         public boolean isFinished() { 
1215                 if (isPreGingerbread
) { 
1216                         return scroller
.isFinished(); 
1218                         return overScroller
.isFinished(); 
1222         public boolean computeScrollOffset() { 
1223                 if (isPreGingerbread
) { 
1224                         return scroller
.computeScrollOffset(); 
1226                         overScroller
.computeScrollOffset(); 
1227                         return overScroller
.computeScrollOffset(); 
1231         public int getCurrX() { 
1232                 if (isPreGingerbread
) { 
1233                         return scroller
.getCurrX(); 
1235                         return overScroller
.getCurrX(); 
1239         public int getCurrY() { 
1240                 if (isPreGingerbread
) { 
1241                         return scroller
.getCurrY(); 
1243                         return overScroller
.getCurrY(); 
1248     @TargetApi(Build
.VERSION_CODES
.JELLY_BEAN
) 
1249         private void compatPostOnAnimation(Runnable runnable
) { 
1250         if (VERSION
.SDK_INT 
>= VERSION_CODES
.JELLY_BEAN
) { 
1251             postOnAnimation(runnable
); 
1254             postDelayed(runnable
, 1000/60); 
1258     private class ZoomVariables 
{ 
1260         public float focusX
; 
1261         public float focusY
; 
1262         public ScaleType scaleType
; 
1264         public ZoomVariables(float scale
, float focusX
, float focusY
, ScaleType scaleType
) { 
1266                 this.focusX 
= focusX
; 
1267                 this.focusY 
= focusY
; 
1268                 this.scaleType 
= scaleType
; 
1272     private void printMatrixInfo() { 
1273         float[] n 
= new float[9]; 
1274         matrix
.getValues(n
); 
1275         Log
.d(DEBUG
, "Scale: " + n
[Matrix
.MSCALE_X
] + " TransX: " + n
[Matrix
.MTRANS_X
] + " TransY: " + n
[Matrix
.MTRANS_Y
]);