1 /* ownCloud Android client application
3 * @author Michael Ortiz
4 * @updated Patrick Lackemacher
9 * Copyright (c) 2012 Michael Ortiz
10 * Copyright (C) 2015 ownCloud Inc.
12 * This program is free software: you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License version 2,
14 * as published by the Free Software Foundation.
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
21 * You should have received a copy of the GNU General Public License
22 * along with this program. If not, see <http://www.gnu.org/licenses/>.
26 package com
.owncloud
.android
.utils
;
28 import com
.owncloud
.android
.ui
.preview
.ImageViewCustom
;
30 import android
.annotation
.TargetApi
;
31 import android
.content
.Context
;
32 import android
.content
.res
.Configuration
;
33 import android
.graphics
.Bitmap
;
34 import android
.graphics
.Canvas
;
35 import android
.graphics
.Matrix
;
36 import android
.graphics
.PointF
;
37 import android
.graphics
.RectF
;
38 import android
.graphics
.drawable
.Drawable
;
39 import android
.net
.Uri
;
40 import android
.os
.Build
;
41 import android
.os
.Build
.VERSION
;
42 import android
.os
.Build
.VERSION_CODES
;
43 import android
.os
.Bundle
;
44 import android
.os
.Parcelable
;
45 import android
.util
.AttributeSet
;
46 import android
.util
.Log
;
47 import android
.view
.GestureDetector
;
48 import android
.view
.MotionEvent
;
49 import android
.view
.ScaleGestureDetector
;
50 import android
.view
.View
;
51 import android
.view
.animation
.AccelerateDecelerateInterpolator
;
52 import android
.widget
.OverScroller
;
53 import android
.widget
.Scroller
;
56 * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom.
58 public class TouchImageViewCustom
extends ImageViewCustom
{
59 private static final String DEBUG
= "DEBUG";
62 // SuperMin and SuperMax multipliers. Determine how much the image can be
63 // zoomed below or above the zoom boundaries, before animating back to the
64 // min/max zoom boundary.
66 private static final float SUPER_MIN_MULTIPLIER
= .75f
;
67 private static final float SUPER_MAX_MULTIPLIER
= 1.25f
;
70 // Scale of image ranges from minScale to maxScale, where minScale == 1
71 // when the image is stretched to fit view.
73 private float normalizedScale
;
76 // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
77 // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
78 // saved prior to the screen rotating.
80 private Matrix matrix
, prevMatrix
;
82 private static enum State
{ NONE
, DRAG
, ZOOM
, FLING
, ANIMATE_ZOOM
};
85 private float minScale
;
86 private float maxScale
;
87 private float superMinScale
;
88 private float superMaxScale
;
91 private Context context
;
94 private ScaleType mScaleType
;
96 private boolean imageRenderedAtLeastOnce
;
97 private boolean onDrawReady
;
99 private ZoomVariables delayedZoomVariables
;
102 // Size of view and previous view size (ie before rotation)
104 private int viewWidth
, viewHeight
, prevViewWidth
, prevViewHeight
;
107 // Size of image when it is stretched to fit view. Before and After rotation.
109 private float matchViewWidth
, matchViewHeight
, prevMatchViewWidth
, prevMatchViewHeight
;
111 private ScaleGestureDetector mScaleDetector
;
112 private GestureDetector mGestureDetector
;
113 private GestureDetector
.OnDoubleTapListener doubleTapListener
= null
;
114 private OnTouchListener userTouchListener
= null
;
115 private OnTouchImageViewListener touchImageViewListener
= null
;
117 public TouchImageViewCustom(Context context
) {
119 sharedConstructing(context
);
122 public TouchImageViewCustom(Context context
, AttributeSet attrs
) {
123 super(context
, attrs
);
124 sharedConstructing(context
);
127 public TouchImageViewCustom(Context context
, AttributeSet attrs
, int defStyle
) {
128 super(context
, attrs
, defStyle
);
129 sharedConstructing(context
);
132 private void sharedConstructing(Context context
) {
133 super.setClickable(true
);
134 this.context
= context
;
135 mScaleDetector
= new ScaleGestureDetector(context
, new ScaleListener());
136 mGestureDetector
= new GestureDetector(context
, new GestureListener());
137 matrix
= new Matrix();
138 prevMatrix
= new Matrix();
141 if (mScaleType
== null
) {
142 mScaleType
= ScaleType
.FIT_CENTER
;
146 superMinScale
= SUPER_MIN_MULTIPLIER
* minScale
;
147 superMaxScale
= SUPER_MAX_MULTIPLIER
* maxScale
;
148 setImageMatrix(matrix
);
149 setScaleType(ScaleType
.MATRIX
);
150 setState(State
.NONE
);
152 super.setOnTouchListener(new PrivateOnTouchListener());
156 public void setOnTouchListener(View
.OnTouchListener l
) {
157 userTouchListener
= l
;
160 public void setOnTouchImageViewListener(OnTouchImageViewListener l
) {
161 touchImageViewListener
= l
;
164 public void setOnDoubleTapListener(GestureDetector
.OnDoubleTapListener l
) {
165 doubleTapListener
= l
;
169 public void setImageResource(int resId
) {
170 super.setImageResource(resId
);
171 savePreviousImageValues();
176 public void setImageBitmap(Bitmap bm
) {
177 super.setImageBitmap(bm
);
178 savePreviousImageValues();
183 public void setImageDrawable(Drawable drawable
) {
184 super.setImageDrawable(drawable
);
185 savePreviousImageValues();
190 public void setImageURI(Uri uri
) {
191 super.setImageURI(uri
);
192 savePreviousImageValues();
197 public void setScaleType(ScaleType type
) {
198 if (type
== ScaleType
.FIT_START
|| type
== ScaleType
.FIT_END
) {
199 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
201 if (type
== ScaleType
.MATRIX
) {
202 super.setScaleType(ScaleType
.MATRIX
);
208 // If the image is already rendered, scaleType has been called programmatically
209 // and the TouchImageView should be updated with the new scaleType.
217 public ScaleType
getScaleType() {
222 * Returns false if image is in initial, unzoomed state. False, otherwise.
223 * @return true if image is zoomed
225 public boolean isZoomed() {
226 return normalizedScale
!= 1;
230 * Return a Rect representing the zoomed image.
231 * @return rect representing zoomed image
233 public RectF
getZoomedRect() {
234 if (mScaleType
== ScaleType
.FIT_XY
) {
235 throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY");
237 PointF topLeft
= transformCoordTouchToBitmap(0, 0, true
);
238 PointF bottomRight
= transformCoordTouchToBitmap(viewWidth
, viewHeight
, true
);
240 float w
= getDrawable().getIntrinsicWidth();
241 float h
= getDrawable().getIntrinsicHeight();
242 return new RectF(topLeft
.x
/ w
, topLeft
.y
/ h
, bottomRight
.x
/ w
, bottomRight
.y
/ h
);
246 * Save the current matrix and view dimensions
247 * in the prevMatrix and prevView variables.
249 private void savePreviousImageValues() {
250 if (matrix
!= null
&& viewHeight
!= 0 && viewWidth
!= 0) {
252 prevMatrix
.setValues(m
);
253 prevMatchViewHeight
= matchViewHeight
;
254 prevMatchViewWidth
= matchViewWidth
;
255 prevViewHeight
= viewHeight
;
256 prevViewWidth
= viewWidth
;
261 public Parcelable
onSaveInstanceState() {
262 Bundle bundle
= new Bundle();
263 bundle
.putParcelable("instanceState", super.onSaveInstanceState());
264 bundle
.putFloat("saveScale", normalizedScale
);
265 bundle
.putFloat("matchViewHeight", matchViewHeight
);
266 bundle
.putFloat("matchViewWidth", matchViewWidth
);
267 bundle
.putInt("viewWidth", viewWidth
);
268 bundle
.putInt("viewHeight", viewHeight
);
270 bundle
.putFloatArray("matrix", m
);
271 bundle
.putBoolean("imageRendered", imageRenderedAtLeastOnce
);
276 public void onRestoreInstanceState(Parcelable state
) {
277 if (state
instanceof Bundle
) {
278 Bundle bundle
= (Bundle
) state
;
279 normalizedScale
= bundle
.getFloat("saveScale");
280 m
= bundle
.getFloatArray("matrix");
281 prevMatrix
.setValues(m
);
282 prevMatchViewHeight
= bundle
.getFloat("matchViewHeight");
283 prevMatchViewWidth
= bundle
.getFloat("matchViewWidth");
284 prevViewHeight
= bundle
.getInt("viewHeight");
285 prevViewWidth
= bundle
.getInt("viewWidth");
286 imageRenderedAtLeastOnce
= bundle
.getBoolean("imageRendered");
287 super.onRestoreInstanceState(bundle
.getParcelable("instanceState"));
291 super.onRestoreInstanceState(state
);
295 protected void onDraw(Canvas canvas
) {
297 imageRenderedAtLeastOnce
= true
;
298 if (delayedZoomVariables
!= null
) {
299 setZoom(delayedZoomVariables
.scale
, delayedZoomVariables
.focusX
, delayedZoomVariables
.focusY
, delayedZoomVariables
.scaleType
);
300 delayedZoomVariables
= null
;
302 super.onDraw(canvas
);
306 public void onConfigurationChanged(Configuration newConfig
) {
307 super.onConfigurationChanged(newConfig
);
308 savePreviousImageValues();
312 * Get the max zoom multiplier.
313 * @return max zoom multiplier.
315 public float getMaxZoom() {
320 * Set the max zoom multiplier. Default value: 3.
321 * @param max max zoom multiplier.
323 public void setMaxZoom(float max
) {
325 superMaxScale
= SUPER_MAX_MULTIPLIER
* maxScale
;
329 * Get the min zoom multiplier.
330 * @return min zoom multiplier.
332 public float getMinZoom() {
337 * Get the current zoom. This is the zoom relative to the initial
338 * scale, not the original resource.
339 * @return current zoom multiplier.
341 public float getCurrentZoom() {
342 return normalizedScale
;
346 * Set the min zoom multiplier. Default value: 1.
347 * @param min min zoom multiplier.
349 public void setMinZoom(float min
) {
351 superMinScale
= SUPER_MIN_MULTIPLIER
* minScale
;
355 * Reset zoom and translation to initial state.
357 public void resetZoom() {
363 * Set zoom to the specified scale. Image will be centered by default.
366 public void setZoom(float scale
) {
367 setZoom(scale
, 0.5f
, 0.5f
);
371 * Set zoom to the specified scale. Image will be centered around the point
372 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
373 * as a fraction from the left and top of the view. For example, the top left
374 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
379 public void setZoom(float scale
, float focusX
, float focusY
) {
380 setZoom(scale
, focusX
, focusY
, mScaleType
);
384 * Set zoom to the specified scale. Image will be centered around the point
385 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
386 * as a fraction from the left and top of the view. For example, the top left
387 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
393 public void setZoom(float scale
, float focusX
, float focusY
, ScaleType scaleType
) {
395 // setZoom can be called before the image is on the screen, but at this point,
396 // image and view sizes have not yet been calculated in onMeasure. Thus, we should
397 // delay calling setZoom until the view has been measured.
400 delayedZoomVariables
= new ZoomVariables(scale
, focusX
, focusY
, scaleType
);
404 if (scaleType
!= mScaleType
) {
405 setScaleType(scaleType
);
408 scaleImage(scale
, viewWidth
/ 2, viewHeight
/ 2, true
);
410 m
[Matrix
.MTRANS_X
] = -((focusX
* getImageWidth()) - (viewWidth
* 0.5f
));
411 m
[Matrix
.MTRANS_Y
] = -((focusY
* getImageHeight()) - (viewHeight
* 0.5f
));
414 setImageMatrix(matrix
);
418 * Set zoom parameters equal to another TouchImageView. Including scale, position,
422 public void setZoom(TouchImageViewCustom img
) {
423 PointF center
= img
.getScrollPosition();
424 setZoom(img
.getCurrentZoom(), center
.x
, center
.y
, img
.getScaleType());
428 * Return the point at the center of the zoomed image. The PointF coordinates range
429 * in value between 0 and 1 and the focus point is denoted as a fraction from the left
430 * and top of the view. For example, the top left corner of the image would be (0, 0).
431 * And the bottom right corner would be (1, 1).
432 * @return PointF representing the scroll position of the zoomed image.
434 public PointF
getScrollPosition() {
435 Drawable drawable
= getDrawable();
436 if (drawable
== null
) {
439 int drawableWidth
= drawable
.getIntrinsicWidth();
440 int drawableHeight
= drawable
.getIntrinsicHeight();
442 PointF point
= transformCoordTouchToBitmap(viewWidth
/ 2, viewHeight
/ 2, true
);
443 point
.x
/= drawableWidth
;
444 point
.y
/= drawableHeight
;
449 * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the
450 * left and top of the view. The focus points can range in value between 0 and 1.
454 public void setScrollPosition(float focusX
, float focusY
) {
455 setZoom(normalizedScale
, focusX
, focusY
);
459 * Performs boundary checking and fixes the image matrix if it
462 private void fixTrans() {
464 float transX
= m
[Matrix
.MTRANS_X
];
465 float transY
= m
[Matrix
.MTRANS_Y
];
467 float fixTransX
= getFixTrans(transX
, viewWidth
, getImageWidth());
468 float fixTransY
= getFixTrans(transY
, viewHeight
, getImageHeight());
470 if (fixTransX
!= 0 || fixTransY
!= 0) {
471 matrix
.postTranslate(fixTransX
, fixTransY
);
476 * When transitioning from zooming from focus to zoom from center (or vice versa)
477 * the image can become unaligned within the view. This is apparent when zooming
478 * quickly. When the content size is less than the view size, the content will often
479 * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
480 * then makes sure the image is centered correctly within the view.
482 private void fixScaleTrans() {
485 if (getImageWidth() < viewWidth
) {
486 m
[Matrix
.MTRANS_X
] = (viewWidth
- getImageWidth()) / 2;
489 if (getImageHeight() < viewHeight
) {
490 m
[Matrix
.MTRANS_Y
] = (viewHeight
- getImageHeight()) / 2;
495 private float getFixTrans(float trans
, float viewSize
, float contentSize
) {
496 float minTrans
, maxTrans
;
498 if (contentSize
<= viewSize
) {
500 maxTrans
= viewSize
- contentSize
;
503 minTrans
= viewSize
- contentSize
;
507 if (trans
< minTrans
)
508 return -trans
+ minTrans
;
509 if (trans
> maxTrans
)
510 return -trans
+ maxTrans
;
514 private float getFixDragTrans(float delta
, float viewSize
, float contentSize
) {
515 if (contentSize
<= viewSize
) {
521 private float getImageWidth() {
522 return matchViewWidth
* normalizedScale
;
525 private float getImageHeight() {
526 return matchViewHeight
* normalizedScale
;
530 protected void onMeasure(int widthMeasureSpec
, int heightMeasureSpec
) {
531 Drawable drawable
= getDrawable();
532 if (drawable
== null
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) {
533 setMeasuredDimension(0, 0);
537 int drawableWidth
= drawable
.getIntrinsicWidth();
538 int drawableHeight
= drawable
.getIntrinsicHeight();
539 int widthSize
= MeasureSpec
.getSize(widthMeasureSpec
);
540 int widthMode
= MeasureSpec
.getMode(widthMeasureSpec
);
541 int heightSize
= MeasureSpec
.getSize(heightMeasureSpec
);
542 int heightMode
= MeasureSpec
.getMode(heightMeasureSpec
);
543 viewWidth
= setViewSize(widthMode
, widthSize
, drawableWidth
);
544 viewHeight
= setViewSize(heightMode
, heightSize
, drawableHeight
);
547 // Set view dimensions
549 setMeasuredDimension(viewWidth
, viewHeight
);
552 // Fit content within view
558 * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
559 * it is made to fit the screen according to the dimensions of the previous image matrix. This
560 * allows the image to maintain its zoom after rotation.
562 private void fitImageToView() {
563 Drawable drawable
= getDrawable();
564 if (drawable
== null
|| drawable
.getIntrinsicWidth() == 0 || drawable
.getIntrinsicHeight() == 0) {
567 if (matrix
== null
|| prevMatrix
== null
) {
571 int drawableWidth
= drawable
.getIntrinsicWidth();
572 int drawableHeight
= drawable
.getIntrinsicHeight();
575 // Scale image for view
577 float scaleX
= (float) viewWidth
/ drawableWidth
;
578 float scaleY
= (float) viewHeight
/ drawableHeight
;
580 switch (mScaleType
) {
586 scaleX
= scaleY
= Math
.max(scaleX
, scaleY
);
590 scaleX
= scaleY
= Math
.min(1, Math
.min(scaleX
, scaleY
));
593 scaleX
= scaleY
= Math
.min(scaleX
, scaleY
);
601 // FIT_START and FIT_END not supported
603 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
610 float redundantXSpace
= viewWidth
- (scaleX
* drawableWidth
);
611 float redundantYSpace
= viewHeight
- (scaleY
* drawableHeight
);
612 matchViewWidth
= viewWidth
- redundantXSpace
;
613 matchViewHeight
= viewHeight
- redundantYSpace
;
614 if (!isZoomed() && !imageRenderedAtLeastOnce
) {
616 // Stretch and center image to fit view
618 matrix
.setScale(scaleX
, scaleY
);
619 matrix
.postTranslate(redundantXSpace
/ 2, redundantYSpace
/ 2);
624 // These values should never be 0 or we will set viewWidth and viewHeight
625 // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues
626 // to set them equal to the current values.
628 if (prevMatchViewWidth
== 0 || prevMatchViewHeight
== 0) {
629 savePreviousImageValues();
632 prevMatrix
.getValues(m
);
635 // Rescale Matrix after rotation
637 m
[Matrix
.MSCALE_X
] = matchViewWidth
/ drawableWidth
* normalizedScale
;
638 m
[Matrix
.MSCALE_Y
] = matchViewHeight
/ drawableHeight
* normalizedScale
;
641 // TransX and TransY from previous matrix
643 float transX
= m
[Matrix
.MTRANS_X
];
644 float transY
= m
[Matrix
.MTRANS_Y
];
649 float prevActualWidth
= prevMatchViewWidth
* normalizedScale
;
650 float actualWidth
= getImageWidth();
651 translateMatrixAfterRotate(Matrix
.MTRANS_X
, transX
, prevActualWidth
, actualWidth
, prevViewWidth
, viewWidth
, drawableWidth
);
656 float prevActualHeight
= prevMatchViewHeight
* normalizedScale
;
657 float actualHeight
= getImageHeight();
658 translateMatrixAfterRotate(Matrix
.MTRANS_Y
, transY
, prevActualHeight
, actualHeight
, prevViewHeight
, viewHeight
, drawableHeight
);
661 // Set the matrix to the adjusted scale and translate values.
666 setImageMatrix(matrix
);
670 * Set view dimensions based on layout params
674 * @param drawableWidth
677 private int setViewSize(int mode
, int size
, int drawableWidth
) {
680 case MeasureSpec
.EXACTLY
:
684 case MeasureSpec
.AT_MOST
:
685 viewSize
= Math
.min(drawableWidth
, size
);
688 case MeasureSpec
.UNSPECIFIED
:
689 viewSize
= drawableWidth
;
700 * After rotating, the matrix needs to be translated. This function finds the area of image
701 * which was previously centered and adjusts translations so that is again the center, post-rotation.
703 * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y
704 * @param trans the value of trans in that axis before the rotation
705 * @param prevImageSize the width/height of the image before the rotation
706 * @param imageSize width/height of the image after rotation
707 * @param prevViewSize width/height of view before rotation
708 * @param viewSize width/height of view after rotation
709 * @param drawableSize width/height of drawable
711 private void translateMatrixAfterRotate(int axis
, float trans
, float prevImageSize
, float imageSize
, int prevViewSize
, int viewSize
, int drawableSize
) {
712 if (imageSize
< viewSize
) {
714 // The width/height of image is less than the view's width/height. Center it.
716 m
[axis
] = (viewSize
- (drawableSize
* m
[Matrix
.MSCALE_X
])) * 0.5f
;
718 } else if (trans
> 0) {
720 // The image is larger than the view, but was not before rotation. Center it.
722 m
[axis
] = -((imageSize
- viewSize
) * 0.5f
);
726 // Find the area of the image which was previously centered in the view. Determine its distance
727 // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage
728 // to calculate the trans in the new view width/height.
730 float percentage
= (Math
.abs(trans
) + (0.5f
* prevViewSize
)) / prevImageSize
;
731 m
[axis
] = -((percentage
* imageSize
) - (viewSize
* 0.5f
));
735 private void setState(State state
) {
739 public boolean canScrollHorizontallyFroyo(int direction
) {
740 return canScrollHorizontally(direction
);
744 public boolean canScrollHorizontally(int direction
) {
746 float x
= m
[Matrix
.MTRANS_X
];
748 if (getImageWidth() < viewWidth
) {
751 } else if (x
>= -1 && direction
< 0) {
754 } else if (Math
.abs(x
) + viewWidth
+ 1 >= getImageWidth() && direction
> 0) {
762 * Gesture Listener detects a single click or long click and passes that on
763 * to the view's listener.
767 private class GestureListener
extends GestureDetector
.SimpleOnGestureListener
{
770 public boolean onSingleTapConfirmed(MotionEvent e
)
772 if(doubleTapListener
!= null
) {
773 return doubleTapListener
.onSingleTapConfirmed(e
);
775 return performClick();
779 public void onLongPress(MotionEvent e
)
785 public boolean onFling(MotionEvent e1
, MotionEvent e2
, float velocityX
, float velocityY
)
789 // If a previous fling is still active, it should be cancelled so that two flings
790 // are not run simultaenously.
794 fling
= new Fling((int) velocityX
, (int) velocityY
);
795 compatPostOnAnimation(fling
);
796 return super.onFling(e1
, e2
, velocityX
, velocityY
);
800 public boolean onDoubleTap(MotionEvent e
) {
801 boolean consumed
= false
;
802 if(doubleTapListener
!= null
) {
803 consumed
= doubleTapListener
.onDoubleTap(e
);
805 if (state
== State
.NONE
) {
806 float targetZoom
= (normalizedScale
== minScale
) ? maxScale
: minScale
;
807 DoubleTapZoom doubleTap
= new DoubleTapZoom(targetZoom
, e
.getX(), e
.getY(), false
);
808 compatPostOnAnimation(doubleTap
);
815 public boolean onDoubleTapEvent(MotionEvent e
) {
816 if(doubleTapListener
!= null
) {
817 return doubleTapListener
.onDoubleTapEvent(e
);
823 public interface OnTouchImageViewListener
{
824 public void onMove();
828 * Responsible for all touch events. Handles the heavy lifting of drag and also sends
829 * touch events to Scale Detector and Gesture Detector.
833 private class PrivateOnTouchListener
implements OnTouchListener
{
836 // Remember last point position for dragging
838 private PointF last
= new PointF();
841 public boolean onTouch(View v
, MotionEvent event
) {
842 mScaleDetector
.onTouchEvent(event
);
843 mGestureDetector
.onTouchEvent(event
);
844 PointF curr
= new PointF(event
.getX(), event
.getY());
846 if (state
== State
.NONE
|| state
== State
.DRAG
|| state
== State
.FLING
) {
847 switch (event
.getAction()) {
848 case MotionEvent
.ACTION_DOWN
:
852 setState(State
.DRAG
);
855 case MotionEvent
.ACTION_MOVE
:
856 if (state
== State
.DRAG
) {
857 float deltaX
= curr
.x
- last
.x
;
858 float deltaY
= curr
.y
- last
.y
;
859 float fixTransX
= getFixDragTrans(deltaX
, viewWidth
, getImageWidth());
860 float fixTransY
= getFixDragTrans(deltaY
, viewHeight
, getImageHeight());
861 matrix
.postTranslate(fixTransX
, fixTransY
);
863 last
.set(curr
.x
, curr
.y
);
867 case MotionEvent
.ACTION_UP
:
868 case MotionEvent
.ACTION_POINTER_UP
:
869 setState(State
.NONE
);
874 setImageMatrix(matrix
);
877 // User-defined OnTouchListener
879 if(userTouchListener
!= null
) {
880 userTouchListener
.onTouch(v
, event
);
884 // OnTouchImageViewListener is set: TouchImageView dragged by user.
886 if (touchImageViewListener
!= null
) {
887 touchImageViewListener
.onMove();
891 // indicate event was handled
898 * ScaleListener detects user two finger scaling and scales image.
902 private class ScaleListener
extends ScaleGestureDetector
.SimpleOnScaleGestureListener
{
904 public boolean onScaleBegin(ScaleGestureDetector detector
) {
905 setState(State
.ZOOM
);
910 public boolean onScale(ScaleGestureDetector detector
) {
911 scaleImage(detector
.getScaleFactor(), detector
.getFocusX(), detector
.getFocusY(), true
);
914 // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
916 if (touchImageViewListener
!= null
) {
917 touchImageViewListener
.onMove();
923 public void onScaleEnd(ScaleGestureDetector detector
) {
924 super.onScaleEnd(detector
);
925 setState(State
.NONE
);
926 boolean animateToZoomBoundary
= false
;
927 float targetZoom
= normalizedScale
;
928 if (normalizedScale
> maxScale
) {
929 targetZoom
= maxScale
;
930 animateToZoomBoundary
= true
;
932 } else if (normalizedScale
< minScale
) {
933 targetZoom
= minScale
;
934 animateToZoomBoundary
= true
;
937 if (animateToZoomBoundary
) {
938 DoubleTapZoom doubleTap
= new DoubleTapZoom(targetZoom
, viewWidth
/ 2, viewHeight
/ 2, true
);
939 compatPostOnAnimation(doubleTap
);
944 private void scaleImage(double deltaScale
, float focusX
, float focusY
, boolean stretchImageToSuper
) {
946 float lowerScale
, upperScale
;
947 if (stretchImageToSuper
) {
948 lowerScale
= superMinScale
;
949 upperScale
= superMaxScale
;
952 lowerScale
= minScale
;
953 upperScale
= maxScale
;
956 float origScale
= normalizedScale
;
957 normalizedScale
*= deltaScale
;
958 if (normalizedScale
> upperScale
) {
959 normalizedScale
= upperScale
;
960 deltaScale
= upperScale
/ origScale
;
961 } else if (normalizedScale
< lowerScale
) {
962 normalizedScale
= lowerScale
;
963 deltaScale
= lowerScale
/ origScale
;
966 matrix
.postScale((float) deltaScale
, (float) deltaScale
, focusX
, focusY
);
971 * DoubleTapZoom calls a series of runnables which apply
972 * an animated zoom in/out graphic to the image.
976 private class DoubleTapZoom
implements Runnable
{
978 private long startTime
;
979 private static final float ZOOM_TIME
= 500;
980 private float startZoom
, targetZoom
;
981 private float bitmapX
, bitmapY
;
982 private boolean stretchImageToSuper
;
983 private AccelerateDecelerateInterpolator interpolator
= new AccelerateDecelerateInterpolator();
984 private PointF startTouch
;
985 private PointF endTouch
;
987 DoubleTapZoom(float targetZoom
, float focusX
, float focusY
, boolean stretchImageToSuper
) {
988 setState(State
.ANIMATE_ZOOM
);
989 startTime
= System
.currentTimeMillis();
990 this.startZoom
= normalizedScale
;
991 this.targetZoom
= targetZoom
;
992 this.stretchImageToSuper
= stretchImageToSuper
;
993 PointF bitmapPoint
= transformCoordTouchToBitmap(focusX
, focusY
, false
);
994 this.bitmapX
= bitmapPoint
.x
;
995 this.bitmapY
= bitmapPoint
.y
;
998 // Used for translating image during scaling
1000 startTouch
= transformCoordBitmapToTouch(bitmapX
, bitmapY
);
1001 endTouch
= new PointF(viewWidth
/ 2, viewHeight
/ 2);
1006 float t
= interpolate();
1007 double deltaScale
= calculateDeltaScale(t
);
1008 scaleImage(deltaScale
, bitmapX
, bitmapY
, stretchImageToSuper
);
1009 translateImageToCenterTouchPosition(t
);
1011 setImageMatrix(matrix
);
1014 // OnTouchImageViewListener is set: double tap runnable updates listener
1015 // with every frame.
1017 if (touchImageViewListener
!= null
) {
1018 touchImageViewListener
.onMove();
1023 // We haven't finished zooming
1025 compatPostOnAnimation(this);
1031 setState(State
.NONE
);
1036 * Interpolate between where the image should start and end in order to translate
1037 * the image so that the point that is touched is what ends up centered at the end
1041 private void translateImageToCenterTouchPosition(float t
) {
1042 float targetX
= startTouch
.x
+ t
* (endTouch
.x
- startTouch
.x
);
1043 float targetY
= startTouch
.y
+ t
* (endTouch
.y
- startTouch
.y
);
1044 PointF curr
= transformCoordBitmapToTouch(bitmapX
, bitmapY
);
1045 matrix
.postTranslate(targetX
- curr
.x
, targetY
- curr
.y
);
1049 * Use interpolator to get t
1052 private float interpolate() {
1053 long currTime
= System
.currentTimeMillis();
1054 float elapsed
= (currTime
- startTime
) / ZOOM_TIME
;
1055 elapsed
= Math
.min(1f
, elapsed
);
1056 return interpolator
.getInterpolation(elapsed
);
1060 * Interpolate the current targeted zoom and get the delta
1061 * from the current zoom.
1065 private double calculateDeltaScale(float t
) {
1066 double zoom
= startZoom
+ t
* (targetZoom
- startZoom
);
1067 return zoom
/ normalizedScale
;
1072 * This function will transform the coordinates in the touch event to the coordinate
1073 * system of the drawable that the imageview contain
1074 * @param x x-coordinate of touch event
1075 * @param y y-coordinate of touch event
1076 * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
1077 * to the bounds of the bitmap size.
1078 * @return Coordinates of the point touched, in the coordinate system of the original drawable.
1080 private PointF
transformCoordTouchToBitmap(float x
, float y
, boolean clipToBitmap
) {
1081 matrix
.getValues(m
);
1082 float origW
= getDrawable().getIntrinsicWidth();
1083 float origH
= getDrawable().getIntrinsicHeight();
1084 float transX
= m
[Matrix
.MTRANS_X
];
1085 float transY
= m
[Matrix
.MTRANS_Y
];
1086 float finalX
= ((x
- transX
) * origW
) / getImageWidth();
1087 float finalY
= ((y
- transY
) * origH
) / getImageHeight();
1090 finalX
= Math
.min(Math
.max(finalX
, 0), origW
);
1091 finalY
= Math
.min(Math
.max(finalY
, 0), origH
);
1094 return new PointF(finalX
, finalY
);
1098 * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
1099 * drawable's coordinate system to the view's coordinate system.
1100 * @param bx x-coordinate in original bitmap coordinate system
1101 * @param by y-coordinate in original bitmap coordinate system
1102 * @return Coordinates of the point in the view's coordinate system.
1104 private PointF
transformCoordBitmapToTouch(float bx
, float by
) {
1105 matrix
.getValues(m
);
1106 float origW
= getDrawable().getIntrinsicWidth();
1107 float origH
= getDrawable().getIntrinsicHeight();
1108 float px
= bx
/ origW
;
1109 float py
= by
/ origH
;
1110 float finalX
= m
[Matrix
.MTRANS_X
] + getImageWidth() * px
;
1111 float finalY
= m
[Matrix
.MTRANS_Y
] + getImageHeight() * py
;
1112 return new PointF(finalX
, finalY
);
1116 * Fling launches sequential runnables which apply
1117 * the fling graphic to the image. The values for the translation
1118 * are interpolated by the Scroller.
1122 private class Fling
implements Runnable
{
1124 CompatScroller scroller
;
1127 Fling(int velocityX
, int velocityY
) {
1128 setState(State
.FLING
);
1129 scroller
= new CompatScroller(context
);
1130 matrix
.getValues(m
);
1132 int startX
= (int) m
[Matrix
.MTRANS_X
];
1133 int startY
= (int) m
[Matrix
.MTRANS_Y
];
1134 int minX
, maxX
, minY
, maxY
;
1136 if (getImageWidth() > viewWidth
) {
1137 minX
= viewWidth
- (int) getImageWidth();
1141 minX
= maxX
= startX
;
1144 if (getImageHeight() > viewHeight
) {
1145 minY
= viewHeight
- (int) getImageHeight();
1149 minY
= maxY
= startY
;
1152 scroller
.fling(startX
, startY
, (int) velocityX
, (int) velocityY
, minX
,
1158 public void cancelFling() {
1159 if (scroller
!= null
) {
1160 setState(State
.NONE
);
1161 scroller
.forceFinished(true
);
1169 // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
1170 // Listener runnable updated with each frame of fling animation.
1172 if (touchImageViewListener
!= null
) {
1173 touchImageViewListener
.onMove();
1176 if (scroller
.isFinished()) {
1181 if (scroller
.computeScrollOffset()) {
1182 int newX
= scroller
.getCurrX();
1183 int newY
= scroller
.getCurrY();
1184 int transX
= newX
- currX
;
1185 int transY
= newY
- currY
;
1188 matrix
.postTranslate(transX
, transY
);
1190 setImageMatrix(matrix
);
1191 compatPostOnAnimation(this);
1196 @TargetApi(Build
.VERSION_CODES
.GINGERBREAD
)
1197 private class CompatScroller
{
1199 OverScroller overScroller
;
1200 boolean isPreGingerbread
;
1202 public CompatScroller(Context context
) {
1203 if (VERSION
.SDK_INT
< VERSION_CODES
.GINGERBREAD
) {
1204 isPreGingerbread
= true
;
1205 scroller
= new Scroller(context
);
1208 isPreGingerbread
= false
;
1209 overScroller
= new OverScroller(context
);
1213 public void fling(int startX
, int startY
, int velocityX
, int velocityY
, int minX
, int maxX
, int minY
, int maxY
) {
1214 if (isPreGingerbread
) {
1215 scroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
);
1217 overScroller
.fling(startX
, startY
, velocityX
, velocityY
, minX
, maxX
, minY
, maxY
);
1221 public void forceFinished(boolean finished
) {
1222 if (isPreGingerbread
) {
1223 scroller
.forceFinished(finished
);
1225 overScroller
.forceFinished(finished
);
1229 public boolean isFinished() {
1230 if (isPreGingerbread
) {
1231 return scroller
.isFinished();
1233 return overScroller
.isFinished();
1237 public boolean computeScrollOffset() {
1238 if (isPreGingerbread
) {
1239 return scroller
.computeScrollOffset();
1241 overScroller
.computeScrollOffset();
1242 return overScroller
.computeScrollOffset();
1246 public int getCurrX() {
1247 if (isPreGingerbread
) {
1248 return scroller
.getCurrX();
1250 return overScroller
.getCurrX();
1254 public int getCurrY() {
1255 if (isPreGingerbread
) {
1256 return scroller
.getCurrY();
1258 return overScroller
.getCurrY();
1263 @TargetApi(Build
.VERSION_CODES
.JELLY_BEAN
)
1264 private void compatPostOnAnimation(Runnable runnable
) {
1265 if (VERSION
.SDK_INT
>= VERSION_CODES
.JELLY_BEAN
) {
1266 postOnAnimation(runnable
);
1269 postDelayed(runnable
, 1000/60);
1273 private class ZoomVariables
{
1275 public float focusX
;
1276 public float focusY
;
1277 public ScaleType scaleType
;
1279 public ZoomVariables(float scale
, float focusX
, float focusY
, ScaleType scaleType
) {
1281 this.focusX
= focusX
;
1282 this.focusY
= focusY
;
1283 this.scaleType
= scaleType
;
1287 private void printMatrixInfo() {
1288 float[] n
= new float[9];
1289 matrix
.getValues(n
);
1290 Log
.d(DEBUG
, "Scale: " + n
[Matrix
.MSCALE_X
] + " TransX: " + n
[Matrix
.MTRANS_X
] + " TransY: " + n
[Matrix
.MTRANS_Y
]);