2 * ownCloud Android client application
4 * @author Michael Ortiz
5 * @updated Patrick Lackemacher
10 * Copyright (c) 2012 Michael Ortiz
11 * Copyright (C) 2015 ownCloud Inc.
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License version 2,
15 * as published by the Free Software Foundation.
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
22 * You should have received a copy of the GNU General Public License
23 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27 package com
.owncloud
.android
.utils
;
29 import com
.owncloud
.android
.ui
.preview
.ImageViewCustom
;
31 import android
.annotation
.TargetApi
;
32 import android
.content
.Context
;
33 import android
.content
.res
.Configuration
;
34 import android
.graphics
.Bitmap
;
35 import android
.graphics
.Canvas
;
36 import android
.graphics
.Matrix
;
37 import android
.graphics
.PointF
;
38 import android
.graphics
.RectF
;
39 import android
.graphics
.drawable
.Drawable
;
40 import android
.net
.Uri
;
41 import android
.os
.Build
;
42 import android
.os
.Build
.VERSION
;
43 import android
.os
.Build
.VERSION_CODES
;
44 import android
.os
.Bundle
;
45 import android
.os
.Parcelable
;
46 import android
.util
.AttributeSet
;
47 import android
.util
.Log
;
48 import android
.view
.GestureDetector
;
49 import android
.view
.MotionEvent
;
50 import android
.view
.ScaleGestureDetector
;
51 import android
.view
.View
;
52 import android
.view
.animation
.AccelerateDecelerateInterpolator
;
53 import android
.widget
.OverScroller
;
54 import android
.widget
.Scroller
;
57 * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom.
59 public class TouchImageViewCustom
extends ImageViewCustom
{
60 private static final String DEBUG
= "DEBUG";
63 // SuperMin and SuperMax multipliers. Determine how much the image can be
64 // zoomed below or above the zoom boundaries, before animating back to the
65 // min/max zoom boundary.
67 private static final float SUPER_MIN_MULTIPLIER
= .75f
;
68 private static final float SUPER_MAX_MULTIPLIER
= 1.25f
;
71 // Scale of image ranges from minScale to maxScale, where minScale == 1
72 // when the image is stretched to fit view.
74 private float normalizedScale
;
77 // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
78 // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
79 // saved prior to the screen rotating.
81 private Matrix matrix
, prevMatrix
;
83 private static enum State
{ NONE
, DRAG
, ZOOM
, FLING
, ANIMATE_ZOOM
};
86 private float minScale
;
87 private float maxScale
;
88 private float superMinScale
;
89 private float superMaxScale
;
92 private Context context
;
95 private ScaleType mScaleType
;
97 private boolean imageRenderedAtLeastOnce
;
98 private boolean onDrawReady
;
100 private ZoomVariables delayedZoomVariables
;
103 // Size of view and previous view size (ie before rotation)
105 private int viewWidth
, viewHeight
, prevViewWidth
, prevViewHeight
;
108 // Size of image when it is stretched to fit view. Before and After rotation.
110 private float matchViewWidth
, matchViewHeight
, prevMatchViewWidth
, prevMatchViewHeight
;
112 private ScaleGestureDetector mScaleDetector
;
113 private GestureDetector mGestureDetector
;
114 private GestureDetector
.OnDoubleTapListener doubleTapListener
= null
;
115 private OnTouchListener userTouchListener
= null
;
116 private OnTouchImageViewListener touchImageViewListener
= null
;
118 public TouchImageViewCustom(Context context
) {
120 sharedConstructing(context
);
123 public TouchImageViewCustom(Context context
, AttributeSet attrs
) {
124 super(context
, attrs
);
125 sharedConstructing(context
);
128 public TouchImageViewCustom(Context context
, AttributeSet attrs
, int defStyle
) {
129 super(context
, attrs
, defStyle
);
130 sharedConstructing(context
);
133 private void sharedConstructing(Context context
) {
134 super.setClickable(true
);
135 this.context
= context
;
136 mScaleDetector
= new ScaleGestureDetector(context
, new ScaleListener());
137 mGestureDetector
= new GestureDetector(context
, new GestureListener());
138 matrix
= new Matrix();
139 prevMatrix
= new Matrix();
142 if (mScaleType
== null
) {
143 mScaleType
= ScaleType
.FIT_CENTER
;
147 superMinScale
= SUPER_MIN_MULTIPLIER
* minScale
;
148 superMaxScale
= SUPER_MAX_MULTIPLIER
* maxScale
;
149 setImageMatrix(matrix
);
150 setScaleType(ScaleType
.MATRIX
);
151 setState(State
.NONE
);
153 super.setOnTouchListener(new PrivateOnTouchListener());
157 public void setOnTouchListener(View
.OnTouchListener l
) {
158 userTouchListener
= l
;
161 public void setOnTouchImageViewListener(OnTouchImageViewListener l
) {
162 touchImageViewListener
= l
;
165 public void setOnDoubleTapListener(GestureDetector
.OnDoubleTapListener l
) {
166 doubleTapListener
= l
;
170 public void setImageResource(int resId
) {
171 super.setImageResource(resId
);
172 savePreviousImageValues();
177 public void setImageBitmap(Bitmap bm
) {
178 super.setImageBitmap(bm
);
179 savePreviousImageValues();
184 public void setImageDrawable(Drawable drawable
) {
185 super.setImageDrawable(drawable
);
186 savePreviousImageValues();
191 public void setImageURI(Uri uri
) {
192 super.setImageURI(uri
);
193 savePreviousImageValues();
198 public void setScaleType(ScaleType type
) {
199 if (type
== ScaleType
.FIT_START
|| type
== ScaleType
.FIT_END
) {
200 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
202 if (type
== ScaleType
.MATRIX
) {
203 super.setScaleType(ScaleType
.MATRIX
);
209 // If the image is already rendered, scaleType has been called programmatically
210 // and the TouchImageView should be updated with the new scaleType.
218 public ScaleType
getScaleType() {
223 * Returns false if image is in initial, unzoomed state. False, otherwise.
224 * @return true if image is zoomed
226 public boolean isZoomed() {
227 return normalizedScale
!= 1;
231 * Return a Rect representing the zoomed image.
232 * @return rect representing zoomed image
234 public RectF
getZoomedRect() {
235 if (mScaleType
== ScaleType
.FIT_XY
) {
236 throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY");
238 PointF topLeft
= transformCoordTouchToBitmap(0, 0, true
);
239 PointF bottomRight
= transformCoordTouchToBitmap(viewWidth
, viewHeight
, true
);
241 float w
= getDrawable().getIntrinsicWidth();
242 float h
= getDrawable().getIntrinsicHeight();
243 return new RectF(topLeft
.x
/ w
, topLeft
.y
/ h
, bottomRight
.x
/ w
, bottomRight
.y
/ h
);
247 * Save the current matrix and view dimensions
248 * in the prevMatrix and prevView variables.
250 private void savePreviousImageValues() {
251 if (matrix
!= null
&& viewHeight
!= 0 && viewWidth
!= 0) {
253 prevMatrix
.setValues(m
);
254 prevMatchViewHeight
= matchViewHeight
;
255 prevMatchViewWidth
= matchViewWidth
;
256 prevViewHeight
= viewHeight
;
257 prevViewWidth
= viewWidth
;
262 public Parcelable
onSaveInstanceState() {
263 Bundle bundle
= new Bundle();
264 bundle
.putParcelable("instanceState", super.onSaveInstanceState());
265 bundle
.putFloat("saveScale", normalizedScale
);
266 bundle
.putFloat("matchViewHeight", matchViewHeight
);
267 bundle
.putFloat("matchViewWidth", matchViewWidth
);
268 bundle
.putInt("viewWidth", viewWidth
);
269 bundle
.putInt("viewHeight", viewHeight
);
271 bundle
.putFloatArray("matrix", m
);
272 bundle
.putBoolean("imageRendered", imageRenderedAtLeastOnce
);
277 public void onRestoreInstanceState(Parcelable state
) {
278 if (state
instanceof Bundle
) {
279 Bundle bundle
= (Bundle
) state
;
280 normalizedScale
= bundle
.getFloat("saveScale");
281 m
= bundle
.getFloatArray("matrix");
282 prevMatrix
.setValues(m
);
283 prevMatchViewHeight
= bundle
.getFloat("matchViewHeight");
284 prevMatchViewWidth
= bundle
.getFloat("matchViewWidth");
285 prevViewHeight
= bundle
.getInt("viewHeight");
286 prevViewWidth
= bundle
.getInt("viewWidth");
287 imageRenderedAtLeastOnce
= bundle
.getBoolean("imageRendered");
288 super.onRestoreInstanceState(bundle
.getParcelable("instanceState"));
292 super.onRestoreInstanceState(state
);
296 protected void onDraw(Canvas canvas
) {
298 imageRenderedAtLeastOnce
= true
;
299 if (delayedZoomVariables
!= null
) {
300 setZoom(delayedZoomVariables
.scale
, delayedZoomVariables
.focusX
, delayedZoomVariables
.focusY
, delayedZoomVariables
.scaleType
);
301 delayedZoomVariables
= null
;
303 super.onDraw(canvas
);
307 public void onConfigurationChanged(Configuration newConfig
) {
308 super.onConfigurationChanged(newConfig
);
309 savePreviousImageValues();
313 * Get the max zoom multiplier.
314 * @return max zoom multiplier.
316 public float getMaxZoom() {
321 * Set the max zoom multiplier. Default value: 3.
322 * @param max max zoom multiplier.
324 public void setMaxZoom(float max
) {
326 superMaxScale
= SUPER_MAX_MULTIPLIER
* maxScale
;
330 * Get the min zoom multiplier.
331 * @return min zoom multiplier.
333 public float getMinZoom() {
338 * Get the current zoom. This is the zoom relative to the initial
339 * scale, not the original resource.
340 * @return current zoom multiplier.
342 public float getCurrentZoom() {
343 return normalizedScale
;
347 * Set the min zoom multiplier. Default value: 1.
348 * @param min min zoom multiplier.
350 public void setMinZoom(float min
) {
352 superMinScale
= SUPER_MIN_MULTIPLIER
* minScale
;
356 * Reset zoom and translation to initial state.
358 public void resetZoom() {
364 * Set zoom to the specified scale. Image will be centered by default.
367 public void setZoom(float scale
) {
368 setZoom(scale
, 0.5f
, 0.5f
);
372 * Set zoom to the specified scale. Image will be centered around the point
373 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
374 * as a fraction from the left and top of the view. For example, the top left
375 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
380 public void setZoom(float scale
, float focusX
, float focusY
) {
381 setZoom(scale
, focusX
, focusY
, mScaleType
);
385 * Set zoom to the specified scale. Image will be centered around the point
386 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
387 * as a fraction from the left and top of the view. For example, the top left
388 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
394 public void setZoom(float scale
, float focusX
, float focusY
, ScaleType scaleType
) {
396 // setZoom can be called before the image is on the screen, but at this point,
397 // image and view sizes have not yet been calculated in onMeasure. Thus, we should
398 // delay calling setZoom until the view has been measured.
401 delayedZoomVariables
= new ZoomVariables(scale
, focusX
, focusY
, scaleType
);
405 if (scaleType
!= mScaleType
) {
406 setScaleType(scaleType
);
409 scaleImage(scale
, viewWidth
/ 2, viewHeight
/ 2, true
);
411 m
[Matrix
.MTRANS_X
] = -((focusX
* getImageWidth()) - (viewWidth
* 0.5f
));
412 m
[Matrix
.MTRANS_Y
] = -((focusY
* getImageHeight()) - (viewHeight
* 0.5f
));
415 setImageMatrix(matrix
);
419 * Set zoom parameters equal to another TouchImageView. Including scale, position,
423 public void setZoom(TouchImageViewCustom img
) {
424 PointF center
= img
.getScrollPosition();
425 setZoom(img
.getCurrentZoom(), center
.x
, center
.y
, img
.getScaleType());
429 * Return the point at the center of the zoomed image. The PointF coordinates range
430 * in value between 0 and 1 and the focus point is denoted as a fraction from the left
431 * and top of the view. For example, the top left corner of the image would be (0, 0).
432 * And the bottom right corner would be (1, 1).
433 * @return PointF representing the scroll position of the zoomed image.
435 public PointF
getScrollPosition() {
436 Drawable drawable
= getDrawable();
437 if (drawable
== null
) {
440 int drawableWidth
= drawable
.getIntrinsicWidth();
441 int drawableHeight
= drawable
.getIntrinsicHeight();
443 PointF point
= transformCoordTouchToBitmap(viewWidth
/ 2, viewHeight
/ 2, true
);
444 point
.x
/= drawableWidth
;
445 point
.y
/= drawableHeight
;
450 * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the
451 * left and top of the view. The focus points can range in value between 0 and 1.
455 public void setScrollPosition(float focusX
, float focusY
) {
456 setZoom(normalizedScale
, focusX
, focusY
);
460 * Performs boundary checking and fixes the image matrix if it
463 private void fixTrans() {
465 float transX
= m
[Matrix
.MTRANS_X
];
466 float transY
= m
[Matrix
.MTRANS_Y
];
468 float fixTransX
= getFixTrans(transX
, viewWidth
, getImageWidth());
469 float fixTransY
= getFixTrans(transY
, viewHeight
, getImageHeight());
471 if (fixTransX
!= 0 || fixTransY
!= 0) {
472 matrix
.postTranslate(fixTransX
, fixTransY
);
477 * When transitioning from zooming from focus to zoom from center (or vice versa)
478 * the image can become unaligned within the view. This is apparent when zooming
479 * quickly. When the content size is less than the view size, the content will often
480 * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
481 * then makes sure the image is centered correctly within the view.
483 private void fixScaleTrans() {
486 if (getImageWidth() < viewWidth
) {
487 m
[Matrix
.MTRANS_X
] = (viewWidth
- getImageWidth()) / 2;
490 if (getImageHeight() < viewHeight
) {
491 m
[Matrix
.MTRANS_Y
] = (viewHeight
- getImageHeight()) / 2;
496 private float getFixTrans(float trans
, float viewSize
, float contentSize
) {
497 float minTrans
, maxTrans
;
499 if (contentSize
<= viewSize
) {
501 maxTrans
= viewSize
- contentSize
;
504 minTrans
= viewSize
- contentSize
;
508 if (trans
< minTrans
)
509 return -trans
+ minTrans
;
510 if (trans
> maxTrans
)
511 return -trans
+ maxTrans
;
515 private float getFixDragTrans(float delta
, float viewSize
, float contentSize
) {
516 if (contentSize
<= viewSize
) {
522 private float getImageWidth() {
523 return matchViewWidth
* normalizedScale
;
526 private float getImageHeight() {
527 return matchViewHeight
* normalizedScale
;
531 protected void onMeasure(int widthMeasureSpec
, int heightMeasureSpec
) {
532 Drawable drawable
= getDrawable();
533 if (drawable
== null
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) {
534 setMeasuredDimension(0, 0);
538 int drawableWidth
= drawable
.getIntrinsicWidth();
539 int drawableHeight
= drawable
.getIntrinsicHeight();
540 int widthSize
= MeasureSpec
.getSize(widthMeasureSpec
);
541 int widthMode
= MeasureSpec
.getMode(widthMeasureSpec
);
542 int heightSize
= MeasureSpec
.getSize(heightMeasureSpec
);
543 int heightMode
= MeasureSpec
.getMode(heightMeasureSpec
);
544 viewWidth
= setViewSize(widthMode
, widthSize
, drawableWidth
);
545 viewHeight
= setViewSize(heightMode
, heightSize
, drawableHeight
);
548 // Set view dimensions
550 setMeasuredDimension(viewWidth
, viewHeight
);
553 // Fit content within view
559 * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
560 * it is made to fit the screen according to the dimensions of the previous image matrix. This
561 * allows the image to maintain its zoom after rotation.
563 private void fitImageToView() {
564 Drawable drawable
= getDrawable();
565 if (drawable
== null
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) {
568 if (matrix
== null
|| prevMatrix
== null
) {
572 int drawableWidth
= drawable
.getIntrinsicWidth();
573 int drawableHeight
= drawable
.getIntrinsicHeight();
576 // Scale image for view
578 float scaleX
= (float) viewWidth
/ drawableWidth
;
579 float scaleY
= (float) viewHeight
/ drawableHeight
;
581 switch (mScaleType
) {
587 scaleX
= scaleY
= Math
.max(scaleX
, scaleY
);
591 scaleX
= scaleY
= Math
.min(1, Math
.min(scaleX
, scaleY
));
594 scaleX
= scaleY
= Math
.min(scaleX
, scaleY
);
602 // FIT_START and FIT_END not supported
604 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
611 float redundantXSpace
= viewWidth
- (scaleX
* drawableWidth
);
612 float redundantYSpace
= viewHeight
- (scaleY
* drawableHeight
);
613 matchViewWidth
= viewWidth
- redundantXSpace
;
614 matchViewHeight
= viewHeight
- redundantYSpace
;
615 if (!isZoomed() && !imageRenderedAtLeastOnce
) {
617 // Stretch and center image to fit view
619 matrix
.setScale(scaleX
, scaleY
);
620 matrix
.postTranslate(redundantXSpace
/ 2, redundantYSpace
/ 2);
625 // These values should never be 0 or we will set viewWidth and viewHeight
626 // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues
627 // to set them equal to the current values.
629 if (prevMatchViewWidth
== 0 || prevMatchViewHeight
== 0) {
630 savePreviousImageValues();
633 prevMatrix
.getValues(m
);
636 // Rescale Matrix after rotation
638 m
[Matrix
.MSCALE_X
] = matchViewWidth
/ drawableWidth
* normalizedScale
;
639 m
[Matrix
.MSCALE_Y
] = matchViewHeight
/ drawableHeight
* normalizedScale
;
642 // TransX and TransY from previous matrix
644 float transX
= m
[Matrix
.MTRANS_X
];
645 float transY
= m
[Matrix
.MTRANS_Y
];
650 float prevActualWidth
= prevMatchViewWidth
* normalizedScale
;
651 float actualWidth
= getImageWidth();
652 translateMatrixAfterRotate(Matrix
.MTRANS_X
, transX
, prevActualWidth
, actualWidth
, prevViewWidth
, viewWidth
, drawableWidth
);
657 float prevActualHeight
= prevMatchViewHeight
* normalizedScale
;
658 float actualHeight
= getImageHeight();
659 translateMatrixAfterRotate(Matrix
.MTRANS_Y
, transY
, prevActualHeight
, actualHeight
, prevViewHeight
, viewHeight
, drawableHeight
);
662 // Set the matrix to the adjusted scale and translate values.
667 setImageMatrix(matrix
);
671 * Set view dimensions based on layout params
675 * @param drawableWidth
678 private int setViewSize(int mode
, int size
, int drawableWidth
) {
681 case MeasureSpec
.EXACTLY
:
685 case MeasureSpec
.AT_MOST
:
686 viewSize
= Math
.min(drawableWidth
, size
);
689 case MeasureSpec
.UNSPECIFIED
:
690 viewSize
= drawableWidth
;
701 * After rotating, the matrix needs to be translated. This function finds the area of image
702 * which was previously centered and adjusts translations so that is again the center, post-rotation.
704 * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y
705 * @param trans the value of trans in that axis before the rotation
706 * @param prevImageSize the width/height of the image before the rotation
707 * @param imageSize width/height of the image after rotation
708 * @param prevViewSize width/height of view before rotation
709 * @param viewSize width/height of view after rotation
710 * @param drawableSize width/height of drawable
712 private void translateMatrixAfterRotate(int axis
, float trans
, float prevImageSize
, float imageSize
, int prevViewSize
, int viewSize
, int drawableSize
) {
713 if (imageSize
< viewSize
) {
715 // The width/height of image is less than the view's width/height. Center it.
717 m
[axis
] = (viewSize
- (drawableSize
* m
[Matrix
.MSCALE_X
])) * 0.5f
;
719 } else if (trans
> 0) {
721 // The image is larger than the view, but was not before rotation. Center it.
723 m
[axis
] = -((imageSize
- viewSize
) * 0.5f
);
727 // Find the area of the image which was previously centered in the view. Determine its distance
728 // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage
729 // to calculate the trans in the new view width/height.
731 float percentage
= (Math
.abs(trans
) + (0.5f
* prevViewSize
)) / prevImageSize
;
732 m
[axis
] = -((percentage
* imageSize
) - (viewSize
* 0.5f
));
736 private void setState(State state
) {
740 public boolean canScrollHorizontallyFroyo(int direction
) {
741 return canScrollHorizontally(direction
);
745 public boolean canScrollHorizontally(int direction
) {
747 float x
= m
[Matrix
.MTRANS_X
];
749 if (getImageWidth() < viewWidth
) {
752 } else if (x
>= -1 && direction
< 0) {
755 } else if (Math
.abs(x
) + viewWidth
+ 1 >= getImageWidth() && direction
> 0) {
763 * Gesture Listener detects a single click or long click and passes that on
764 * to the view's listener.
768 private class GestureListener
extends GestureDetector
.SimpleOnGestureListener
{
771 public boolean onSingleTapConfirmed(MotionEvent e
)
773 if(doubleTapListener
!= null
) {
774 return doubleTapListener
.onSingleTapConfirmed(e
);
776 return performClick();
780 public void onLongPress(MotionEvent e
)
786 public boolean onFling(MotionEvent e1
, MotionEvent e2
, float velocityX
, float velocityY
)
790 // If a previous fling is still active, it should be cancelled so that two flings
791 // are not run simultaenously.
795 fling
= new Fling((int) velocityX
, (int) velocityY
);
796 compatPostOnAnimation(fling
);
797 return super.onFling(e1
, e2
, velocityX
, velocityY
);
801 public boolean onDoubleTap(MotionEvent e
) {
802 boolean consumed
= false
;
803 if(doubleTapListener
!= null
) {
804 consumed
= doubleTapListener
.onDoubleTap(e
);
806 if (state
== State
.NONE
) {
807 float targetZoom
= (normalizedScale
== minScale
) ? maxScale
: minScale
;
808 DoubleTapZoom doubleTap
= new DoubleTapZoom(targetZoom
, e
.getX(), e
.getY(), false
);
809 compatPostOnAnimation(doubleTap
);
816 public boolean onDoubleTapEvent(MotionEvent e
) {
817 if(doubleTapListener
!= null
) {
818 return doubleTapListener
.onDoubleTapEvent(e
);
824 public interface OnTouchImageViewListener
{
825 public void onMove();
829 * Responsible for all touch events. Handles the heavy lifting of drag and also sends
830 * touch events to Scale Detector and Gesture Detector.
834 private class PrivateOnTouchListener
implements OnTouchListener
{
837 // Remember last point position for dragging
839 private PointF last
= new PointF();
842 public boolean onTouch(View v
, MotionEvent event
) {
843 mScaleDetector
.onTouchEvent(event
);
844 mGestureDetector
.onTouchEvent(event
);
845 PointF curr
= new PointF(event
.getX(), event
.getY());
847 if (state
== State
.NONE
|| state
== State
.DRAG
|| state
== State
.FLING
) {
848 switch (event
.getAction()) {
849 case MotionEvent
.ACTION_DOWN
:
853 setState(State
.DRAG
);
856 case MotionEvent
.ACTION_MOVE
:
857 if (state
== State
.DRAG
) {
858 float deltaX
= curr
.x
- last
.x
;
859 float deltaY
= curr
.y
- last
.y
;
860 float fixTransX
= getFixDragTrans(deltaX
, viewWidth
, getImageWidth());
861 float fixTransY
= getFixDragTrans(deltaY
, viewHeight
, getImageHeight());
862 matrix
.postTranslate(fixTransX
, fixTransY
);
864 last
.set(curr
.x
, curr
.y
);
868 case MotionEvent
.ACTION_UP
:
869 case MotionEvent
.ACTION_POINTER_UP
:
870 setState(State
.NONE
);
875 setImageMatrix(matrix
);
878 // User-defined OnTouchListener
880 if(userTouchListener
!= null
) {
881 userTouchListener
.onTouch(v
, event
);
885 // OnTouchImageViewListener is set: TouchImageView dragged by user.
887 if (touchImageViewListener
!= null
) {
888 touchImageViewListener
.onMove();
892 // indicate event was handled
899 * ScaleListener detects user two finger scaling and scales image.
903 private class ScaleListener
extends ScaleGestureDetector
.SimpleOnScaleGestureListener
{
905 public boolean onScaleBegin(ScaleGestureDetector detector
) {
906 setState(State
.ZOOM
);
911 public boolean onScale(ScaleGestureDetector detector
) {
912 scaleImage(detector
.getScaleFactor(), detector
.getFocusX(), detector
.getFocusY(), true
);
915 // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
917 if (touchImageViewListener
!= null
) {
918 touchImageViewListener
.onMove();
924 public void onScaleEnd(ScaleGestureDetector detector
) {
925 super.onScaleEnd(detector
);
926 setState(State
.NONE
);
927 boolean animateToZoomBoundary
= false
;
928 float targetZoom
= normalizedScale
;
929 if (normalizedScale
> maxScale
) {
930 targetZoom
= maxScale
;
931 animateToZoomBoundary
= true
;
933 } else if (normalizedScale
< minScale
) {
934 targetZoom
= minScale
;
935 animateToZoomBoundary
= true
;
938 if (animateToZoomBoundary
) {
939 DoubleTapZoom doubleTap
= new DoubleTapZoom(targetZoom
, viewWidth
/ 2, viewHeight
/ 2, true
);
940 compatPostOnAnimation(doubleTap
);
945 private void scaleImage(double deltaScale
, float focusX
, float focusY
, boolean stretchImageToSuper
) {
947 float lowerScale
, upperScale
;
948 if (stretchImageToSuper
) {
949 lowerScale
= superMinScale
;
950 upperScale
= superMaxScale
;
953 lowerScale
= minScale
;
954 upperScale
= maxScale
;
957 float origScale
= normalizedScale
;
958 normalizedScale
*= deltaScale
;
959 if (normalizedScale
> upperScale
) {
960 normalizedScale
= upperScale
;
961 deltaScale
= upperScale
/ origScale
;
962 } else if (normalizedScale
< lowerScale
) {
963 normalizedScale
= lowerScale
;
964 deltaScale
= lowerScale
/ origScale
;
967 matrix
.postScale((float) deltaScale
, (float) deltaScale
, focusX
, focusY
);
972 * DoubleTapZoom calls a series of runnables which apply
973 * an animated zoom in/out graphic to the image.
977 private class DoubleTapZoom
implements Runnable
{
979 private long startTime
;
980 private static final float ZOOM_TIME
= 500;
981 private float startZoom
, targetZoom
;
982 private float bitmapX
, bitmapY
;
983 private boolean stretchImageToSuper
;
984 private AccelerateDecelerateInterpolator interpolator
= new AccelerateDecelerateInterpolator();
985 private PointF startTouch
;
986 private PointF endTouch
;
988 DoubleTapZoom(float targetZoom
, float focusX
, float focusY
, boolean stretchImageToSuper
) {
989 setState(State
.ANIMATE_ZOOM
);
990 startTime
= System
.currentTimeMillis();
991 this.startZoom
= normalizedScale
;
992 this.targetZoom
= targetZoom
;
993 this.stretchImageToSuper
= stretchImageToSuper
;
994 PointF bitmapPoint
= transformCoordTouchToBitmap(focusX
, focusY
, false
);
995 this.bitmapX
= bitmapPoint
.x
;
996 this.bitmapY
= bitmapPoint
.y
;
999 // Used for translating image during scaling
1001 startTouch
= transformCoordBitmapToTouch(bitmapX
, bitmapY
);
1002 endTouch
= new PointF(viewWidth
/ 2, viewHeight
/ 2);
1007 float t
= interpolate();
1008 double deltaScale
= calculateDeltaScale(t
);
1009 scaleImage(deltaScale
, bitmapX
, bitmapY
, stretchImageToSuper
);
1010 translateImageToCenterTouchPosition(t
);
1012 setImageMatrix(matrix
);
1015 // OnTouchImageViewListener is set: double tap runnable updates listener
1016 // with every frame.
1018 if (touchImageViewListener
!= null
) {
1019 touchImageViewListener
.onMove();
1024 // We haven't finished zooming
1026 compatPostOnAnimation(this);
1032 setState(State
.NONE
);
1037 * Interpolate between where the image should start and end in order to translate
1038 * the image so that the point that is touched is what ends up centered at the end
1042 private void translateImageToCenterTouchPosition(float t
) {
1043 float targetX
= startTouch
.x
+ t
* (endTouch
.x
- startTouch
.x
);
1044 float targetY
= startTouch
.y
+ t
* (endTouch
.y
- startTouch
.y
);
1045 PointF curr
= transformCoordBitmapToTouch(bitmapX
, bitmapY
);
1046 matrix
.postTranslate(targetX
- curr
.x
, targetY
- curr
.y
);
1050 * Use interpolator to get t
1053 private float interpolate() {
1054 long currTime
= System
.currentTimeMillis();
1055 float elapsed
= (currTime
- startTime
) / ZOOM_TIME
;
1056 elapsed
= Math
.min(1f
, elapsed
);
1057 return interpolator
.getInterpolation(elapsed
);
1061 * Interpolate the current targeted zoom and get the delta
1062 * from the current zoom.
1066 private double calculateDeltaScale(float t
) {
1067 double zoom
= startZoom
+ t
* (targetZoom
- startZoom
);
1068 return zoom
/ normalizedScale
;
1073 * This function will transform the coordinates in the touch event to the coordinate
1074 * system of the drawable that the imageview contain
1075 * @param x x-coordinate of touch event
1076 * @param y y-coordinate of touch event
1077 * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
1078 * to the bounds of the bitmap size.
1079 * @return Coordinates of the point touched, in the coordinate system of the original drawable.
1081 private PointF
transformCoordTouchToBitmap(float x
, float y
, boolean clipToBitmap
) {
1082 matrix
.getValues(m
);
1083 float origW
= getDrawable().getIntrinsicWidth();
1084 float origH
= getDrawable().getIntrinsicHeight();
1085 float transX
= m
[Matrix
.MTRANS_X
];
1086 float transY
= m
[Matrix
.MTRANS_Y
];
1087 float finalX
= ((x
- transX
) * origW
) / getImageWidth();
1088 float finalY
= ((y
- transY
) * origH
) / getImageHeight();
1091 finalX
= Math
.min(Math
.max(finalX
, 0), origW
);
1092 finalY
= Math
.min(Math
.max(finalY
, 0), origH
);
1095 return new PointF(finalX
, finalY
);
1099 * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
1100 * drawable's coordinate system to the view's coordinate system.
1101 * @param bx x-coordinate in original bitmap coordinate system
1102 * @param by y-coordinate in original bitmap coordinate system
1103 * @return Coordinates of the point in the view's coordinate system.
1105 private PointF
transformCoordBitmapToTouch(float bx
, float by
) {
1106 matrix
.getValues(m
);
1107 float origW
= getDrawable().getIntrinsicWidth();
1108 float origH
= getDrawable().getIntrinsicHeight();
1109 float px
= bx
/ origW
;
1110 float py
= by
/ origH
;
1111 float finalX
= m
[Matrix
.MTRANS_X
] + getImageWidth() * px
;
1112 float finalY
= m
[Matrix
.MTRANS_Y
] + getImageHeight() * py
;
1113 return new PointF(finalX
, finalY
);
1117 * Fling launches sequential runnables which apply
1118 * the fling graphic to the image. The values for the translation
1119 * are interpolated by the Scroller.
1123 private class Fling
implements Runnable
{
1125 CompatScroller scroller
;
1128 Fling(int velocityX
, int velocityY
) {
1129 setState(State
.FLING
);
1130 scroller
= new CompatScroller(context
);
1131 matrix
.getValues(m
);
1133 int startX
= (int) m
[Matrix
.MTRANS_X
];
1134 int startY
= (int) m
[Matrix
.MTRANS_Y
];
1135 int minX
, maxX
, minY
, maxY
;
1137 if (getImageWidth() > viewWidth
) {
1138 minX
= viewWidth
- (int) getImageWidth();
1142 minX
= maxX
= startX
;
1145 if (getImageHeight() > viewHeight
) {
1146 minY
= viewHeight
- (int) getImageHeight();
1150 minY
= maxY
= startY
;
1153 scroller
.fling(startX
, startY
, (int) velocityX
, (int) velocityY
, minX
,
1159 public void cancelFling() {
1160 if (scroller
!= null
) {
1161 setState(State
.NONE
);
1162 scroller
.forceFinished(true
);
1170 // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
1171 // Listener runnable updated with each frame of fling animation.
1173 if (touchImageViewListener
!= null
) {
1174 touchImageViewListener
.onMove();
1177 if (scroller
.isFinished()) {
1182 if (scroller
.computeScrollOffset()) {
1183 int newX
= scroller
.getCurrX();
1184 int newY
= scroller
.getCurrY();
1185 int transX
= newX
- currX
;
1186 int transY
= newY
- currY
;
1189 matrix
.postTranslate(transX
, transY
);
1191 setImageMatrix(matrix
);
1192 compatPostOnAnimation(this);
1197 @TargetApi(Build
.VERSION_CODES
.GINGERBREAD
)
1198 private class CompatScroller
{
1200 OverScroller overScroller
;
1201 boolean isPreGingerbread
;
1203 public CompatScroller(Context context
) {
1204 if (VERSION
.SDK_INT
< VERSION_CODES
.GINGERBREAD
) {
1205 isPreGingerbread
= true
;
1206 scroller
= new Scroller(context
);
1209 isPreGingerbread
= false
;
1210 overScroller
= new OverScroller(context
);
1214 public void fling(int startX
, int startY
, int velocityX
, int velocityY
, int minX
, int maxX
, int minY
, int maxY
) {
1215 if (isPreGingerbread
) {
1216 scroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
);
1218 overScroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
);
1222 public void forceFinished(boolean finished
) {
1223 if (isPreGingerbread
) {
1224 scroller
.forceFinished(finished
);
1226 overScroller
.forceFinished(finished
);
1230 public boolean isFinished() {
1231 if (isPreGingerbread
) {
1232 return scroller
.isFinished();
1234 return overScroller
.isFinished();
1238 public boolean computeScrollOffset() {
1239 if (isPreGingerbread
) {
1240 return scroller
.computeScrollOffset();
1242 overScroller
.computeScrollOffset();
1243 return overScroller
.computeScrollOffset();
1247 public int getCurrX() {
1248 if (isPreGingerbread
) {
1249 return scroller
.getCurrX();
1251 return overScroller
.getCurrX();
1255 public int getCurrY() {
1256 if (isPreGingerbread
) {
1257 return scroller
.getCurrY();
1259 return overScroller
.getCurrY();
1264 @TargetApi(Build
.VERSION_CODES
.JELLY_BEAN
)
1265 private void compatPostOnAnimation(Runnable runnable
) {
1266 if (VERSION
.SDK_INT
>= VERSION_CODES
.JELLY_BEAN
) {
1267 postOnAnimation(runnable
);
1270 postDelayed(runnable
, 1000/60);
1274 private class ZoomVariables
{
1276 public float focusX
;
1277 public float focusY
;
1278 public ScaleType scaleType
;
1280 public ZoomVariables(float scale
, float focusX
, float focusY
, ScaleType scaleType
) {
1282 this.focusX
= focusX
;
1283 this.focusY
= focusY
;
1284 this.scaleType
= scaleType
;
1288 private void printMatrixInfo() {
1289 float[] n
= new float[9];
1290 matrix
.getValues(n
);
1291 Log
.d(DEBUG
, "Scale: " + n
[Matrix
.MSCALE_X
] + " TransX: " + n
[Matrix
.MTRANS_X
] + " TransY: " + n
[Matrix
.MTRANS_Y
]);