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
]);