4 * Updated By: Patrick Lackemacher
6 * Updated By: @ipsilondev
8 * Updated By: singpolyma
10 * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom.
13 package com
.owncloud
.android
.utils
;
15 import com
.owncloud
.android
.ui
.preview
.ImageViewCustom
;
17 import android
.annotation
.TargetApi
;
18 import android
.content
.Context
;
19 import android
.content
.res
.Configuration
;
20 import android
.graphics
.Bitmap
;
21 import android
.graphics
.Canvas
;
22 import android
.graphics
.Matrix
;
23 import android
.graphics
.PointF
;
24 import android
.graphics
.RectF
;
25 import android
.graphics
.drawable
.Drawable
;
26 import android
.net
.Uri
;
27 import android
.os
.Build
;
28 import android
.os
.Build
.VERSION
;
29 import android
.os
.Build
.VERSION_CODES
;
30 import android
.os
.Bundle
;
31 import android
.os
.Parcelable
;
32 import android
.util
.AttributeSet
;
33 import android
.util
.Log
;
34 import android
.view
.GestureDetector
;
35 import android
.view
.MotionEvent
;
36 import android
.view
.ScaleGestureDetector
;
37 import android
.view
.View
;
38 import android
.view
.animation
.AccelerateDecelerateInterpolator
;
39 import android
.widget
.OverScroller
;
40 import android
.widget
.Scroller
;
42 public class TouchImageViewCustom
extends ImageViewCustom
{
43 private static final String DEBUG
= "DEBUG";
46 // SuperMin and SuperMax multipliers. Determine how much the image can be
47 // zoomed below or above the zoom boundaries, before animating back to the
48 // min/max zoom boundary.
50 private static final float SUPER_MIN_MULTIPLIER
= .75f
;
51 private static final float SUPER_MAX_MULTIPLIER
= 1.25f
;
54 // Scale of image ranges from minScale to maxScale, where minScale == 1
55 // when the image is stretched to fit view.
57 private float normalizedScale
;
60 // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
61 // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
62 // saved prior to the screen rotating.
64 private Matrix matrix
, prevMatrix
;
66 private static enum State
{ NONE
, DRAG
, ZOOM
, FLING
, ANIMATE_ZOOM
};
69 private float minScale
;
70 private float maxScale
;
71 private float superMinScale
;
72 private float superMaxScale
;
75 private Context context
;
78 private ScaleType mScaleType
;
80 private boolean imageRenderedAtLeastOnce
;
81 private boolean onDrawReady
;
83 private ZoomVariables delayedZoomVariables
;
86 // Size of view and previous view size (ie before rotation)
88 private int viewWidth
, viewHeight
, prevViewWidth
, prevViewHeight
;
91 // Size of image when it is stretched to fit view. Before and After rotation.
93 private float matchViewWidth
, matchViewHeight
, prevMatchViewWidth
, prevMatchViewHeight
;
95 private ScaleGestureDetector mScaleDetector
;
96 private GestureDetector mGestureDetector
;
97 private GestureDetector
.OnDoubleTapListener doubleTapListener
= null
;
98 private OnTouchListener userTouchListener
= null
;
99 private OnTouchImageViewListener touchImageViewListener
= null
;
101 public TouchImageViewCustom(Context context
) {
103 sharedConstructing(context
);
106 public TouchImageViewCustom(Context context
, AttributeSet attrs
) {
107 super(context
, attrs
);
108 sharedConstructing(context
);
111 public TouchImageViewCustom(Context context
, AttributeSet attrs
, int defStyle
) {
112 super(context
, attrs
, defStyle
);
113 sharedConstructing(context
);
116 private void sharedConstructing(Context context
) {
117 super.setClickable(true
);
118 this.context
= context
;
119 mScaleDetector
= new ScaleGestureDetector(context
, new ScaleListener());
120 mGestureDetector
= new GestureDetector(context
, new GestureListener());
121 matrix
= new Matrix();
122 prevMatrix
= new Matrix();
125 if (mScaleType
== null
) {
126 mScaleType
= ScaleType
.FIT_CENTER
;
130 superMinScale
= SUPER_MIN_MULTIPLIER
* minScale
;
131 superMaxScale
= SUPER_MAX_MULTIPLIER
* maxScale
;
132 setImageMatrix(matrix
);
133 setScaleType(ScaleType
.MATRIX
);
134 setState(State
.NONE
);
136 super.setOnTouchListener(new PrivateOnTouchListener());
140 public void setOnTouchListener(View
.OnTouchListener l
) {
141 userTouchListener
= l
;
144 public void setOnTouchImageViewListener(OnTouchImageViewListener l
) {
145 touchImageViewListener
= l
;
148 public void setOnDoubleTapListener(GestureDetector
.OnDoubleTapListener l
) {
149 doubleTapListener
= l
;
153 public void setImageResource(int resId
) {
154 super.setImageResource(resId
);
155 savePreviousImageValues();
160 public void setImageBitmap(Bitmap bm
) {
161 super.setImageBitmap(bm
);
162 savePreviousImageValues();
167 public void setImageDrawable(Drawable drawable
) {
168 super.setImageDrawable(drawable
);
169 savePreviousImageValues();
174 public void setImageURI(Uri uri
) {
175 super.setImageURI(uri
);
176 savePreviousImageValues();
181 public void setScaleType(ScaleType type
) {
182 if (type
== ScaleType
.FIT_START
|| type
== ScaleType
.FIT_END
) {
183 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
185 if (type
== ScaleType
.MATRIX
) {
186 super.setScaleType(ScaleType
.MATRIX
);
192 // If the image is already rendered, scaleType has been called programmatically
193 // and the TouchImageView should be updated with the new scaleType.
201 public ScaleType
getScaleType() {
206 * Returns false if image is in initial, unzoomed state. False, otherwise.
207 * @return true if image is zoomed
209 public boolean isZoomed() {
210 return normalizedScale
!= 1;
214 * Return a Rect representing the zoomed image.
215 * @return rect representing zoomed image
217 public RectF
getZoomedRect() {
218 if (mScaleType
== ScaleType
.FIT_XY
) {
219 throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY");
221 PointF topLeft
= transformCoordTouchToBitmap(0, 0, true
);
222 PointF bottomRight
= transformCoordTouchToBitmap(viewWidth
, viewHeight
, true
);
224 float w
= getDrawable().getIntrinsicWidth();
225 float h
= getDrawable().getIntrinsicHeight();
226 return new RectF(topLeft
.x
/ w
, topLeft
.y
/ h
, bottomRight
.x
/ w
, bottomRight
.y
/ h
);
230 * Save the current matrix and view dimensions
231 * in the prevMatrix and prevView variables.
233 private void savePreviousImageValues() {
234 if (matrix
!= null
&& viewHeight
!= 0 && viewWidth
!= 0) {
236 prevMatrix
.setValues(m
);
237 prevMatchViewHeight
= matchViewHeight
;
238 prevMatchViewWidth
= matchViewWidth
;
239 prevViewHeight
= viewHeight
;
240 prevViewWidth
= viewWidth
;
245 public Parcelable
onSaveInstanceState() {
246 Bundle bundle
= new Bundle();
247 bundle
.putParcelable("instanceState", super.onSaveInstanceState());
248 bundle
.putFloat("saveScale", normalizedScale
);
249 bundle
.putFloat("matchViewHeight", matchViewHeight
);
250 bundle
.putFloat("matchViewWidth", matchViewWidth
);
251 bundle
.putInt("viewWidth", viewWidth
);
252 bundle
.putInt("viewHeight", viewHeight
);
254 bundle
.putFloatArray("matrix", m
);
255 bundle
.putBoolean("imageRendered", imageRenderedAtLeastOnce
);
260 public void onRestoreInstanceState(Parcelable state
) {
261 if (state
instanceof Bundle
) {
262 Bundle bundle
= (Bundle
) state
;
263 normalizedScale
= bundle
.getFloat("saveScale");
264 m
= bundle
.getFloatArray("matrix");
265 prevMatrix
.setValues(m
);
266 prevMatchViewHeight
= bundle
.getFloat("matchViewHeight");
267 prevMatchViewWidth
= bundle
.getFloat("matchViewWidth");
268 prevViewHeight
= bundle
.getInt("viewHeight");
269 prevViewWidth
= bundle
.getInt("viewWidth");
270 imageRenderedAtLeastOnce
= bundle
.getBoolean("imageRendered");
271 super.onRestoreInstanceState(bundle
.getParcelable("instanceState"));
275 super.onRestoreInstanceState(state
);
279 protected void onDraw(Canvas canvas
) {
281 imageRenderedAtLeastOnce
= true
;
282 if (delayedZoomVariables
!= null
) {
283 setZoom(delayedZoomVariables
.scale
, delayedZoomVariables
.focusX
, delayedZoomVariables
.focusY
, delayedZoomVariables
.scaleType
);
284 delayedZoomVariables
= null
;
286 super.onDraw(canvas
);
290 public void onConfigurationChanged(Configuration newConfig
) {
291 super.onConfigurationChanged(newConfig
);
292 savePreviousImageValues();
296 * Get the max zoom multiplier.
297 * @return max zoom multiplier.
299 public float getMaxZoom() {
304 * Set the max zoom multiplier. Default value: 3.
305 * @param max max zoom multiplier.
307 public void setMaxZoom(float max
) {
309 superMaxScale
= SUPER_MAX_MULTIPLIER
* maxScale
;
313 * Get the min zoom multiplier.
314 * @return min zoom multiplier.
316 public float getMinZoom() {
321 * Get the current zoom. This is the zoom relative to the initial
322 * scale, not the original resource.
323 * @return current zoom multiplier.
325 public float getCurrentZoom() {
326 return normalizedScale
;
330 * Set the min zoom multiplier. Default value: 1.
331 * @param min min zoom multiplier.
333 public void setMinZoom(float min
) {
335 superMinScale
= SUPER_MIN_MULTIPLIER
* minScale
;
339 * Reset zoom and translation to initial state.
341 public void resetZoom() {
347 * Set zoom to the specified scale. Image will be centered by default.
350 public void setZoom(float scale
) {
351 setZoom(scale
, 0.5f
, 0.5f
);
355 * Set zoom to the specified scale. Image will be centered around the point
356 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
357 * as a fraction from the left and top of the view. For example, the top left
358 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
363 public void setZoom(float scale
, float focusX
, float focusY
) {
364 setZoom(scale
, focusX
, focusY
, mScaleType
);
368 * Set zoom to the specified scale. Image will be centered around the point
369 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
370 * as a fraction from the left and top of the view. For example, the top left
371 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
377 public void setZoom(float scale
, float focusX
, float focusY
, ScaleType scaleType
) {
379 // setZoom can be called before the image is on the screen, but at this point,
380 // image and view sizes have not yet been calculated in onMeasure. Thus, we should
381 // delay calling setZoom until the view has been measured.
384 delayedZoomVariables
= new ZoomVariables(scale
, focusX
, focusY
, scaleType
);
388 if (scaleType
!= mScaleType
) {
389 setScaleType(scaleType
);
392 scaleImage(scale
, viewWidth
/ 2, viewHeight
/ 2, true
);
394 m
[Matrix
.MTRANS_X
] = -((focusX
* getImageWidth()) - (viewWidth
* 0.5f
));
395 m
[Matrix
.MTRANS_Y
] = -((focusY
* getImageHeight()) - (viewHeight
* 0.5f
));
398 setImageMatrix(matrix
);
402 * Set zoom parameters equal to another TouchImageView. Including scale, position,
404 * @param TouchImageView
406 public void setZoom(TouchImageViewCustom img
) {
407 PointF center
= img
.getScrollPosition();
408 setZoom(img
.getCurrentZoom(), center
.x
, center
.y
, img
.getScaleType());
412 * Return the point at the center of the zoomed image. The PointF coordinates range
413 * in value between 0 and 1 and the focus point is denoted as a fraction from the left
414 * and top of the view. For example, the top left corner of the image would be (0, 0).
415 * And the bottom right corner would be (1, 1).
416 * @return PointF representing the scroll position of the zoomed image.
418 public PointF
getScrollPosition() {
419 Drawable drawable
= getDrawable();
420 if (drawable
== null
) {
423 int drawableWidth
= drawable
.getIntrinsicWidth();
424 int drawableHeight
= drawable
.getIntrinsicHeight();
426 PointF point
= transformCoordTouchToBitmap(viewWidth
/ 2, viewHeight
/ 2, true
);
427 point
.x
/= drawableWidth
;
428 point
.y
/= drawableHeight
;
433 * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the
434 * left and top of the view. The focus points can range in value between 0 and 1.
438 public void setScrollPosition(float focusX
, float focusY
) {
439 setZoom(normalizedScale
, focusX
, focusY
);
443 * Performs boundary checking and fixes the image matrix if it
446 private void fixTrans() {
448 float transX
= m
[Matrix
.MTRANS_X
];
449 float transY
= m
[Matrix
.MTRANS_Y
];
451 float fixTransX
= getFixTrans(transX
, viewWidth
, getImageWidth());
452 float fixTransY
= getFixTrans(transY
, viewHeight
, getImageHeight());
454 if (fixTransX
!= 0 || fixTransY
!= 0) {
455 matrix
.postTranslate(fixTransX
, fixTransY
);
460 * When transitioning from zooming from focus to zoom from center (or vice versa)
461 * the image can become unaligned within the view. This is apparent when zooming
462 * quickly. When the content size is less than the view size, the content will often
463 * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
464 * then makes sure the image is centered correctly within the view.
466 private void fixScaleTrans() {
469 if (getImageWidth() < viewWidth
) {
470 m
[Matrix
.MTRANS_X
] = (viewWidth
- getImageWidth()) / 2;
473 if (getImageHeight() < viewHeight
) {
474 m
[Matrix
.MTRANS_Y
] = (viewHeight
- getImageHeight()) / 2;
479 private float getFixTrans(float trans
, float viewSize
, float contentSize
) {
480 float minTrans
, maxTrans
;
482 if (contentSize
<= viewSize
) {
484 maxTrans
= viewSize
- contentSize
;
487 minTrans
= viewSize
- contentSize
;
491 if (trans
< minTrans
)
492 return -trans
+ minTrans
;
493 if (trans
> maxTrans
)
494 return -trans
+ maxTrans
;
498 private float getFixDragTrans(float delta
, float viewSize
, float contentSize
) {
499 if (contentSize
<= viewSize
) {
505 private float getImageWidth() {
506 return matchViewWidth
* normalizedScale
;
509 private float getImageHeight() {
510 return matchViewHeight
* normalizedScale
;
514 protected void onMeasure(int widthMeasureSpec
, int heightMeasureSpec
) {
515 Drawable drawable
= getDrawable();
516 if (drawable
== null
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) {
517 setMeasuredDimension(0, 0);
521 int drawableWidth
= drawable
.getIntrinsicWidth();
522 int drawableHeight
= drawable
.getIntrinsicHeight();
523 int widthSize
= MeasureSpec
.getSize(widthMeasureSpec
);
524 int widthMode
= MeasureSpec
.getMode(widthMeasureSpec
);
525 int heightSize
= MeasureSpec
.getSize(heightMeasureSpec
);
526 int heightMode
= MeasureSpec
.getMode(heightMeasureSpec
);
527 viewWidth
= setViewSize(widthMode
, widthSize
, drawableWidth
);
528 viewHeight
= setViewSize(heightMode
, heightSize
, drawableHeight
);
531 // Set view dimensions
533 setMeasuredDimension(viewWidth
, viewHeight
);
536 // Fit content within view
542 * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
543 * it is made to fit the screen according to the dimensions of the previous image matrix. This
544 * allows the image to maintain its zoom after rotation.
546 private void fitImageToView() {
547 Drawable drawable
= getDrawable();
548 if (drawable
== null
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) {
551 if (matrix
== null
|| prevMatrix
== null
) {
555 int drawableWidth
= drawable
.getIntrinsicWidth();
556 int drawableHeight
= drawable
.getIntrinsicHeight();
559 // Scale image for view
561 float scaleX
= (float) viewWidth
/ drawableWidth
;
562 float scaleY
= (float) viewHeight
/ drawableHeight
;
564 switch (mScaleType
) {
570 scaleX
= scaleY
= Math
.max(scaleX
, scaleY
);
574 scaleX
= scaleY
= Math
.min(1, Math
.min(scaleX
, scaleY
));
577 scaleX
= scaleY
= Math
.min(scaleX
, scaleY
);
585 // FIT_START and FIT_END not supported
587 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
594 float redundantXSpace
= viewWidth
- (scaleX
* drawableWidth
);
595 float redundantYSpace
= viewHeight
- (scaleY
* drawableHeight
);
596 matchViewWidth
= viewWidth
- redundantXSpace
;
597 matchViewHeight
= viewHeight
- redundantYSpace
;
598 if (!isZoomed() && !imageRenderedAtLeastOnce
) {
600 // Stretch and center image to fit view
602 matrix
.setScale(scaleX
, scaleY
);
603 matrix
.postTranslate(redundantXSpace
/ 2, redundantYSpace
/ 2);
608 // These values should never be 0 or we will set viewWidth and viewHeight
609 // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues
610 // to set them equal to the current values.
612 if (prevMatchViewWidth
== 0 || prevMatchViewHeight
== 0) {
613 savePreviousImageValues();
616 prevMatrix
.getValues(m
);
619 // Rescale Matrix after rotation
621 m
[Matrix
.MSCALE_X
] = matchViewWidth
/ drawableWidth
* normalizedScale
;
622 m
[Matrix
.MSCALE_Y
] = matchViewHeight
/ drawableHeight
* normalizedScale
;
625 // TransX and TransY from previous matrix
627 float transX
= m
[Matrix
.MTRANS_X
];
628 float transY
= m
[Matrix
.MTRANS_Y
];
633 float prevActualWidth
= prevMatchViewWidth
* normalizedScale
;
634 float actualWidth
= getImageWidth();
635 translateMatrixAfterRotate(Matrix
.MTRANS_X
, transX
, prevActualWidth
, actualWidth
, prevViewWidth
, viewWidth
, drawableWidth
);
640 float prevActualHeight
= prevMatchViewHeight
* normalizedScale
;
641 float actualHeight
= getImageHeight();
642 translateMatrixAfterRotate(Matrix
.MTRANS_Y
, transY
, prevActualHeight
, actualHeight
, prevViewHeight
, viewHeight
, drawableHeight
);
645 // Set the matrix to the adjusted scale and translate values.
650 setImageMatrix(matrix
);
654 * Set view dimensions based on layout params
658 * @param drawableWidth
661 private int setViewSize(int mode
, int size
, int drawableWidth
) {
664 case MeasureSpec
.EXACTLY
:
668 case MeasureSpec
.AT_MOST
:
669 viewSize
= Math
.min(drawableWidth
, size
);
672 case MeasureSpec
.UNSPECIFIED
:
673 viewSize
= drawableWidth
;
684 * After rotating, the matrix needs to be translated. This function finds the area of image
685 * which was previously centered and adjusts translations so that is again the center, post-rotation.
687 * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y
688 * @param trans the value of trans in that axis before the rotation
689 * @param prevImageSize the width/height of the image before the rotation
690 * @param imageSize width/height of the image after rotation
691 * @param prevViewSize width/height of view before rotation
692 * @param viewSize width/height of view after rotation
693 * @param drawableSize width/height of drawable
695 private void translateMatrixAfterRotate(int axis
, float trans
, float prevImageSize
, float imageSize
, int prevViewSize
, int viewSize
, int drawableSize
) {
696 if (imageSize
< viewSize
) {
698 // The width/height of image is less than the view's width/height. Center it.
700 m
[axis
] = (viewSize
- (drawableSize
* m
[Matrix
.MSCALE_X
])) * 0.5f
;
702 } else if (trans
> 0) {
704 // The image is larger than the view, but was not before rotation. Center it.
706 m
[axis
] = -((imageSize
- viewSize
) * 0.5f
);
710 // Find the area of the image which was previously centered in the view. Determine its distance
711 // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage
712 // to calculate the trans in the new view width/height.
714 float percentage
= (Math
.abs(trans
) + (0.5f
* prevViewSize
)) / prevImageSize
;
715 m
[axis
] = -((percentage
* imageSize
) - (viewSize
* 0.5f
));
719 private void setState(State state
) {
723 public boolean canScrollHorizontallyFroyo(int direction
) {
724 return canScrollHorizontally(direction
);
728 public boolean canScrollHorizontally(int direction
) {
730 float x
= m
[Matrix
.MTRANS_X
];
732 if (getImageWidth() < viewWidth
) {
735 } else if (x
>= -1 && direction
< 0) {
738 } else if (Math
.abs(x
) + viewWidth
+ 1 >= getImageWidth() && direction
> 0) {
746 * Gesture Listener detects a single click or long click and passes that on
747 * to the view's listener.
751 private class GestureListener
extends GestureDetector
.SimpleOnGestureListener
{
754 public boolean onSingleTapConfirmed(MotionEvent e
)
756 if(doubleTapListener
!= null
) {
757 return doubleTapListener
.onSingleTapConfirmed(e
);
759 return performClick();
763 public void onLongPress(MotionEvent e
)
769 public boolean onFling(MotionEvent e1
, MotionEvent e2
, float velocityX
, float velocityY
)
773 // If a previous fling is still active, it should be cancelled so that two flings
774 // are not run simultaenously.
778 fling
= new Fling((int) velocityX
, (int) velocityY
);
779 compatPostOnAnimation(fling
);
780 return super.onFling(e1
, e2
, velocityX
, velocityY
);
784 public boolean onDoubleTap(MotionEvent e
) {
785 boolean consumed
= false
;
786 if(doubleTapListener
!= null
) {
787 consumed
= doubleTapListener
.onDoubleTap(e
);
789 if (state
== State
.NONE
) {
790 float targetZoom
= (normalizedScale
== minScale
) ? maxScale
: minScale
;
791 DoubleTapZoom doubleTap
= new DoubleTapZoom(targetZoom
, e
.getX(), e
.getY(), false
);
792 compatPostOnAnimation(doubleTap
);
799 public boolean onDoubleTapEvent(MotionEvent e
) {
800 if(doubleTapListener
!= null
) {
801 return doubleTapListener
.onDoubleTapEvent(e
);
807 public interface OnTouchImageViewListener
{
808 public void onMove();
812 * Responsible for all touch events. Handles the heavy lifting of drag and also sends
813 * touch events to Scale Detector and Gesture Detector.
817 private class PrivateOnTouchListener
implements OnTouchListener
{
820 // Remember last point position for dragging
822 private PointF last
= new PointF();
825 public boolean onTouch(View v
, MotionEvent event
) {
826 mScaleDetector
.onTouchEvent(event
);
827 mGestureDetector
.onTouchEvent(event
);
828 PointF curr
= new PointF(event
.getX(), event
.getY());
830 if (state
== State
.NONE
|| state
== State
.DRAG
|| state
== State
.FLING
) {
831 switch (event
.getAction()) {
832 case MotionEvent
.ACTION_DOWN
:
836 setState(State
.DRAG
);
839 case MotionEvent
.ACTION_MOVE
:
840 if (state
== State
.DRAG
) {
841 float deltaX
= curr
.x
- last
.x
;
842 float deltaY
= curr
.y
- last
.y
;
843 float fixTransX
= getFixDragTrans(deltaX
, viewWidth
, getImageWidth());
844 float fixTransY
= getFixDragTrans(deltaY
, viewHeight
, getImageHeight());
845 matrix
.postTranslate(fixTransX
, fixTransY
);
847 last
.set(curr
.x
, curr
.y
);
851 case MotionEvent
.ACTION_UP
:
852 case MotionEvent
.ACTION_POINTER_UP
:
853 setState(State
.NONE
);
858 setImageMatrix(matrix
);
861 // User-defined OnTouchListener
863 if(userTouchListener
!= null
) {
864 userTouchListener
.onTouch(v
, event
);
868 // OnTouchImageViewListener is set: TouchImageView dragged by user.
870 if (touchImageViewListener
!= null
) {
871 touchImageViewListener
.onMove();
875 // indicate event was handled
882 * ScaleListener detects user two finger scaling and scales image.
886 private class ScaleListener
extends ScaleGestureDetector
.SimpleOnScaleGestureListener
{
888 public boolean onScaleBegin(ScaleGestureDetector detector
) {
889 setState(State
.ZOOM
);
894 public boolean onScale(ScaleGestureDetector detector
) {
895 scaleImage(detector
.getScaleFactor(), detector
.getFocusX(), detector
.getFocusY(), true
);
898 // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
900 if (touchImageViewListener
!= null
) {
901 touchImageViewListener
.onMove();
907 public void onScaleEnd(ScaleGestureDetector detector
) {
908 super.onScaleEnd(detector
);
909 setState(State
.NONE
);
910 boolean animateToZoomBoundary
= false
;
911 float targetZoom
= normalizedScale
;
912 if (normalizedScale
> maxScale
) {
913 targetZoom
= maxScale
;
914 animateToZoomBoundary
= true
;
916 } else if (normalizedScale
< minScale
) {
917 targetZoom
= minScale
;
918 animateToZoomBoundary
= true
;
921 if (animateToZoomBoundary
) {
922 DoubleTapZoom doubleTap
= new DoubleTapZoom(targetZoom
, viewWidth
/ 2, viewHeight
/ 2, true
);
923 compatPostOnAnimation(doubleTap
);
928 private void scaleImage(double deltaScale
, float focusX
, float focusY
, boolean stretchImageToSuper
) {
930 float lowerScale
, upperScale
;
931 if (stretchImageToSuper
) {
932 lowerScale
= superMinScale
;
933 upperScale
= superMaxScale
;
936 lowerScale
= minScale
;
937 upperScale
= maxScale
;
940 float origScale
= normalizedScale
;
941 normalizedScale
*= deltaScale
;
942 if (normalizedScale
> upperScale
) {
943 normalizedScale
= upperScale
;
944 deltaScale
= upperScale
/ origScale
;
945 } else if (normalizedScale
< lowerScale
) {
946 normalizedScale
= lowerScale
;
947 deltaScale
= lowerScale
/ origScale
;
950 matrix
.postScale((float) deltaScale
, (float) deltaScale
, focusX
, focusY
);
955 * DoubleTapZoom calls a series of runnables which apply
956 * an animated zoom in/out graphic to the image.
960 private class DoubleTapZoom
implements Runnable
{
962 private long startTime
;
963 private static final float ZOOM_TIME
= 500;
964 private float startZoom
, targetZoom
;
965 private float bitmapX
, bitmapY
;
966 private boolean stretchImageToSuper
;
967 private AccelerateDecelerateInterpolator interpolator
= new AccelerateDecelerateInterpolator();
968 private PointF startTouch
;
969 private PointF endTouch
;
971 DoubleTapZoom(float targetZoom
, float focusX
, float focusY
, boolean stretchImageToSuper
) {
972 setState(State
.ANIMATE_ZOOM
);
973 startTime
= System
.currentTimeMillis();
974 this.startZoom
= normalizedScale
;
975 this.targetZoom
= targetZoom
;
976 this.stretchImageToSuper
= stretchImageToSuper
;
977 PointF bitmapPoint
= transformCoordTouchToBitmap(focusX
, focusY
, false
);
978 this.bitmapX
= bitmapPoint
.x
;
979 this.bitmapY
= bitmapPoint
.y
;
982 // Used for translating image during scaling
984 startTouch
= transformCoordBitmapToTouch(bitmapX
, bitmapY
);
985 endTouch
= new PointF(viewWidth
/ 2, viewHeight
/ 2);
990 float t
= interpolate();
991 double deltaScale
= calculateDeltaScale(t
);
992 scaleImage(deltaScale
, bitmapX
, bitmapY
, stretchImageToSuper
);
993 translateImageToCenterTouchPosition(t
);
995 setImageMatrix(matrix
);
998 // OnTouchImageViewListener is set: double tap runnable updates listener
1001 if (touchImageViewListener
!= null
) {
1002 touchImageViewListener
.onMove();
1007 // We haven't finished zooming
1009 compatPostOnAnimation(this);
1015 setState(State
.NONE
);
1020 * Interpolate between where the image should start and end in order to translate
1021 * the image so that the point that is touched is what ends up centered at the end
1025 private void translateImageToCenterTouchPosition(float t
) {
1026 float targetX
= startTouch
.x
+ t
* (endTouch
.x
- startTouch
.x
);
1027 float targetY
= startTouch
.y
+ t
* (endTouch
.y
- startTouch
.y
);
1028 PointF curr
= transformCoordBitmapToTouch(bitmapX
, bitmapY
);
1029 matrix
.postTranslate(targetX
- curr
.x
, targetY
- curr
.y
);
1033 * Use interpolator to get t
1036 private float interpolate() {
1037 long currTime
= System
.currentTimeMillis();
1038 float elapsed
= (currTime
- startTime
) / ZOOM_TIME
;
1039 elapsed
= Math
.min(1f
, elapsed
);
1040 return interpolator
.getInterpolation(elapsed
);
1044 * Interpolate the current targeted zoom and get the delta
1045 * from the current zoom.
1049 private double calculateDeltaScale(float t
) {
1050 double zoom
= startZoom
+ t
* (targetZoom
- startZoom
);
1051 return zoom
/ normalizedScale
;
1056 * This function will transform the coordinates in the touch event to the coordinate
1057 * system of the drawable that the imageview contain
1058 * @param x x-coordinate of touch event
1059 * @param y y-coordinate of touch event
1060 * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
1061 * to the bounds of the bitmap size.
1062 * @return Coordinates of the point touched, in the coordinate system of the original drawable.
1064 private PointF
transformCoordTouchToBitmap(float x
, float y
, boolean clipToBitmap
) {
1065 matrix
.getValues(m
);
1066 float origW
= getDrawable().getIntrinsicWidth();
1067 float origH
= getDrawable().getIntrinsicHeight();
1068 float transX
= m
[Matrix
.MTRANS_X
];
1069 float transY
= m
[Matrix
.MTRANS_Y
];
1070 float finalX
= ((x
- transX
) * origW
) / getImageWidth();
1071 float finalY
= ((y
- transY
) * origH
) / getImageHeight();
1074 finalX
= Math
.min(Math
.max(finalX
, 0), origW
);
1075 finalY
= Math
.min(Math
.max(finalY
, 0), origH
);
1078 return new PointF(finalX
, finalY
);
1082 * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
1083 * drawable's coordinate system to the view's coordinate system.
1084 * @param bx x-coordinate in original bitmap coordinate system
1085 * @param by y-coordinate in original bitmap coordinate system
1086 * @return Coordinates of the point in the view's coordinate system.
1088 private PointF
transformCoordBitmapToTouch(float bx
, float by
) {
1089 matrix
.getValues(m
);
1090 float origW
= getDrawable().getIntrinsicWidth();
1091 float origH
= getDrawable().getIntrinsicHeight();
1092 float px
= bx
/ origW
;
1093 float py
= by
/ origH
;
1094 float finalX
= m
[Matrix
.MTRANS_X
] + getImageWidth() * px
;
1095 float finalY
= m
[Matrix
.MTRANS_Y
] + getImageHeight() * py
;
1096 return new PointF(finalX
, finalY
);
1100 * Fling launches sequential runnables which apply
1101 * the fling graphic to the image. The values for the translation
1102 * are interpolated by the Scroller.
1106 private class Fling
implements Runnable
{
1108 CompatScroller scroller
;
1111 Fling(int velocityX
, int velocityY
) {
1112 setState(State
.FLING
);
1113 scroller
= new CompatScroller(context
);
1114 matrix
.getValues(m
);
1116 int startX
= (int) m
[Matrix
.MTRANS_X
];
1117 int startY
= (int) m
[Matrix
.MTRANS_Y
];
1118 int minX
, maxX
, minY
, maxY
;
1120 if (getImageWidth() > viewWidth
) {
1121 minX
= viewWidth
- (int) getImageWidth();
1125 minX
= maxX
= startX
;
1128 if (getImageHeight() > viewHeight
) {
1129 minY
= viewHeight
- (int) getImageHeight();
1133 minY
= maxY
= startY
;
1136 scroller
.fling(startX
, startY
, (int) velocityX
, (int) velocityY
, minX
,
1142 public void cancelFling() {
1143 if (scroller
!= null
) {
1144 setState(State
.NONE
);
1145 scroller
.forceFinished(true
);
1153 // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
1154 // Listener runnable updated with each frame of fling animation.
1156 if (touchImageViewListener
!= null
) {
1157 touchImageViewListener
.onMove();
1160 if (scroller
.isFinished()) {
1165 if (scroller
.computeScrollOffset()) {
1166 int newX
= scroller
.getCurrX();
1167 int newY
= scroller
.getCurrY();
1168 int transX
= newX
- currX
;
1169 int transY
= newY
- currY
;
1172 matrix
.postTranslate(transX
, transY
);
1174 setImageMatrix(matrix
);
1175 compatPostOnAnimation(this);
1180 @TargetApi(Build
.VERSION_CODES
.GINGERBREAD
)
1181 private class CompatScroller
{
1183 OverScroller overScroller
;
1184 boolean isPreGingerbread
;
1186 public CompatScroller(Context context
) {
1187 if (VERSION
.SDK_INT
< VERSION_CODES
.GINGERBREAD
) {
1188 isPreGingerbread
= true
;
1189 scroller
= new Scroller(context
);
1192 isPreGingerbread
= false
;
1193 overScroller
= new OverScroller(context
);
1197 public void fling(int startX
, int startY
, int velocityX
, int velocityY
, int minX
, int maxX
, int minY
, int maxY
) {
1198 if (isPreGingerbread
) {
1199 scroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
);
1201 overScroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
);
1205 public void forceFinished(boolean finished
) {
1206 if (isPreGingerbread
) {
1207 scroller
.forceFinished(finished
);
1209 overScroller
.forceFinished(finished
);
1213 public boolean isFinished() {
1214 if (isPreGingerbread
) {
1215 return scroller
.isFinished();
1217 return overScroller
.isFinished();
1221 public boolean computeScrollOffset() {
1222 if (isPreGingerbread
) {
1223 return scroller
.computeScrollOffset();
1225 overScroller
.computeScrollOffset();
1226 return overScroller
.computeScrollOffset();
1230 public int getCurrX() {
1231 if (isPreGingerbread
) {
1232 return scroller
.getCurrX();
1234 return overScroller
.getCurrX();
1238 public int getCurrY() {
1239 if (isPreGingerbread
) {
1240 return scroller
.getCurrY();
1242 return overScroller
.getCurrY();
1247 @TargetApi(Build
.VERSION_CODES
.JELLY_BEAN
)
1248 private void compatPostOnAnimation(Runnable runnable
) {
1249 if (VERSION
.SDK_INT
>= VERSION_CODES
.JELLY_BEAN
) {
1250 postOnAnimation(runnable
);
1253 postDelayed(runnable
, 1000/60);
1257 private class ZoomVariables
{
1259 public float focusX
;
1260 public float focusY
;
1261 public ScaleType scaleType
;
1263 public ZoomVariables(float scale
, float focusX
, float focusY
, ScaleType scaleType
) {
1265 this.focusX
= focusX
;
1266 this.focusY
= focusY
;
1267 this.scaleType
= scaleType
;
1271 private void printMatrixInfo() {
1272 float[] n
= new float[9];
1273 matrix
.getValues(n
);
1274 Log
.d(DEBUG
, "Scale: " + n
[Matrix
.MSCALE_X
] + " TransX: " + n
[Matrix
.MTRANS_X
] + " TransY: " + n
[Matrix
.MTRANS_Y
]);