Changes from comments in PR
[pub/Android/ownCloud.git] / src / third_parties / michaelOrtiz / TouchImageViewCustom.java
1 /**
2 * @author Michael Ortiz
3 * @updated Patrick Lackemacher
4 * @updated Babay88
5 * @updated @ipsilondev
6 * @updated hank-cp
7 * @updated singpolyma
8 * Copyright (c) 2012 Michael Ortiz
9 */
10
11 package third_parties.michaelOrtiz;
12
13 import com.owncloud.android.ui.preview.ImageViewCustom;
14
15 import android.annotation.TargetApi;
16 import android.content.Context;
17 import android.content.res.Configuration;
18 import android.graphics.Bitmap;
19 import android.graphics.Canvas;
20 import android.graphics.Matrix;
21 import android.graphics.PointF;
22 import android.graphics.RectF;
23 import android.graphics.drawable.Drawable;
24 import android.net.Uri;
25 import android.os.Build;
26 import android.os.Build.VERSION;
27 import android.os.Build.VERSION_CODES;
28 import android.os.Bundle;
29 import android.os.Parcelable;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.GestureDetector;
33 import android.view.MotionEvent;
34 import android.view.ScaleGestureDetector;
35 import android.view.View;
36 import android.view.animation.AccelerateDecelerateInterpolator;
37 import android.widget.OverScroller;
38 import android.widget.Scroller;
39
40 /**
41 * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom.
42 */
43 public class TouchImageViewCustom extends ImageViewCustom {
44 private static final String DEBUG = "DEBUG";
45
46 //
47 // SuperMin and SuperMax multipliers. Determine how much the image can be
48 // zoomed below or above the zoom boundaries, before animating back to the
49 // min/max zoom boundary.
50 //
51 private static final float SUPER_MIN_MULTIPLIER = .75f;
52 private static final float SUPER_MAX_MULTIPLIER = 1.25f;
53
54 //
55 // Scale of image ranges from minScale to maxScale, where minScale == 1
56 // when the image is stretched to fit view.
57 //
58 private float normalizedScale;
59
60 //
61 // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
62 // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
63 // saved prior to the screen rotating.
64 //
65 private Matrix matrix, prevMatrix;
66
67 private static enum State { NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM };
68 private State state;
69
70 private float minScale;
71 private float maxScale;
72 private float superMinScale;
73 private float superMaxScale;
74 private float[] m;
75
76 private Context context;
77 private Fling fling;
78
79 private ScaleType mScaleType;
80
81 private boolean imageRenderedAtLeastOnce;
82 private boolean onDrawReady;
83
84 private ZoomVariables delayedZoomVariables;
85
86 //
87 // Size of view and previous view size (ie before rotation)
88 //
89 private int viewWidth, viewHeight, prevViewWidth, prevViewHeight;
90
91 //
92 // Size of image when it is stretched to fit view. Before and After rotation.
93 //
94 private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight;
95
96 private ScaleGestureDetector mScaleDetector;
97 private GestureDetector mGestureDetector;
98 private GestureDetector.OnDoubleTapListener doubleTapListener = null;
99 private OnTouchListener userTouchListener = null;
100 private OnTouchImageViewListener touchImageViewListener = null;
101
102 public TouchImageViewCustom(Context context) {
103 super(context);
104 sharedConstructing(context);
105 }
106
107 public TouchImageViewCustom(Context context, AttributeSet attrs) {
108 super(context, attrs);
109 sharedConstructing(context);
110 }
111
112 public TouchImageViewCustom(Context context, AttributeSet attrs, int defStyle) {
113 super(context, attrs, defStyle);
114 sharedConstructing(context);
115 }
116
117 private void sharedConstructing(Context context) {
118 super.setClickable(true);
119 this.context = context;
120 mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
121 mGestureDetector = new GestureDetector(context, new GestureListener());
122 matrix = new Matrix();
123 prevMatrix = new Matrix();
124 m = new float[9];
125 normalizedScale = 1;
126 if (mScaleType == null) {
127 mScaleType = ScaleType.FIT_CENTER;
128 }
129 minScale = 1;
130 maxScale = 3;
131 superMinScale = SUPER_MIN_MULTIPLIER * minScale;
132 superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
133 setImageMatrix(matrix);
134 setScaleType(ScaleType.MATRIX);
135 setState(State.NONE);
136 onDrawReady = false;
137 super.setOnTouchListener(new PrivateOnTouchListener());
138 }
139
140 @Override
141 public void setOnTouchListener(View.OnTouchListener l) {
142 userTouchListener = l;
143 }
144
145 public void setOnTouchImageViewListener(OnTouchImageViewListener l) {
146 touchImageViewListener = l;
147 }
148
149 public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener l) {
150 doubleTapListener = l;
151 }
152
153 @Override
154 public void setImageResource(int resId) {
155 super.setImageResource(resId);
156 savePreviousImageValues();
157 fitImageToView();
158 }
159
160 @Override
161 public void setImageBitmap(Bitmap bm) {
162 super.setImageBitmap(bm);
163 savePreviousImageValues();
164 fitImageToView();
165 }
166
167 @Override
168 public void setImageDrawable(Drawable drawable) {
169 super.setImageDrawable(drawable);
170 savePreviousImageValues();
171 fitImageToView();
172 }
173
174 @Override
175 public void setImageURI(Uri uri) {
176 super.setImageURI(uri);
177 savePreviousImageValues();
178 fitImageToView();
179 }
180
181 @Override
182 public void setScaleType(ScaleType type) {
183 if (type == ScaleType.FIT_START || type == ScaleType.FIT_END) {
184 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
185 }
186 if (type == ScaleType.MATRIX) {
187 super.setScaleType(ScaleType.MATRIX);
188
189 } else {
190 mScaleType = type;
191 if (onDrawReady) {
192 //
193 // If the image is already rendered, scaleType has been called programmatically
194 // and the TouchImageView should be updated with the new scaleType.
195 //
196 setZoom(this);
197 }
198 }
199 }
200
201 @Override
202 public ScaleType getScaleType() {
203 return mScaleType;
204 }
205
206 /**
207 * Returns false if image is in initial, unzoomed state. False, otherwise.
208 * @return true if image is zoomed
209 */
210 public boolean isZoomed() {
211 return normalizedScale != 1;
212 }
213
214 /**
215 * Return a Rect representing the zoomed image.
216 * @return rect representing zoomed image
217 */
218 public RectF getZoomedRect() {
219 if (mScaleType == ScaleType.FIT_XY) {
220 throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY");
221 }
222 PointF topLeft = transformCoordTouchToBitmap(0, 0, true);
223 PointF bottomRight = transformCoordTouchToBitmap(viewWidth, viewHeight, true);
224
225 float w = getDrawable().getIntrinsicWidth();
226 float h = getDrawable().getIntrinsicHeight();
227 return new RectF(topLeft.x / w, topLeft.y / h, bottomRight.x / w, bottomRight.y / h);
228 }
229
230 /**
231 * Save the current matrix and view dimensions
232 * in the prevMatrix and prevView variables.
233 */
234 private void savePreviousImageValues() {
235 if (matrix != null && viewHeight != 0 && viewWidth != 0) {
236 matrix.getValues(m);
237 prevMatrix.setValues(m);
238 prevMatchViewHeight = matchViewHeight;
239 prevMatchViewWidth = matchViewWidth;
240 prevViewHeight = viewHeight;
241 prevViewWidth = viewWidth;
242 }
243 }
244
245 @Override
246 public Parcelable onSaveInstanceState() {
247 Bundle bundle = new Bundle();
248 bundle.putParcelable("instanceState", super.onSaveInstanceState());
249 bundle.putFloat("saveScale", normalizedScale);
250 bundle.putFloat("matchViewHeight", matchViewHeight);
251 bundle.putFloat("matchViewWidth", matchViewWidth);
252 bundle.putInt("viewWidth", viewWidth);
253 bundle.putInt("viewHeight", viewHeight);
254 matrix.getValues(m);
255 bundle.putFloatArray("matrix", m);
256 bundle.putBoolean("imageRendered", imageRenderedAtLeastOnce);
257 return bundle;
258 }
259
260 @Override
261 public void onRestoreInstanceState(Parcelable state) {
262 if (state instanceof Bundle) {
263 Bundle bundle = (Bundle) state;
264 normalizedScale = bundle.getFloat("saveScale");
265 m = bundle.getFloatArray("matrix");
266 prevMatrix.setValues(m);
267 prevMatchViewHeight = bundle.getFloat("matchViewHeight");
268 prevMatchViewWidth = bundle.getFloat("matchViewWidth");
269 prevViewHeight = bundle.getInt("viewHeight");
270 prevViewWidth = bundle.getInt("viewWidth");
271 imageRenderedAtLeastOnce = bundle.getBoolean("imageRendered");
272 super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
273 return;
274 }
275
276 super.onRestoreInstanceState(state);
277 }
278
279 @Override
280 protected void onDraw(Canvas canvas) {
281 onDrawReady = true;
282 imageRenderedAtLeastOnce = true;
283 if (delayedZoomVariables != null) {
284 setZoom(delayedZoomVariables.scale, delayedZoomVariables.focusX, delayedZoomVariables.focusY, delayedZoomVariables.scaleType);
285 delayedZoomVariables = null;
286 }
287 super.onDraw(canvas);
288 }
289
290 @Override
291 public void onConfigurationChanged(Configuration newConfig) {
292 super.onConfigurationChanged(newConfig);
293 savePreviousImageValues();
294 }
295
296 /**
297 * Get the max zoom multiplier.
298 * @return max zoom multiplier.
299 */
300 public float getMaxZoom() {
301 return maxScale;
302 }
303
304 /**
305 * Set the max zoom multiplier. Default value: 3.
306 * @param max max zoom multiplier.
307 */
308 public void setMaxZoom(float max) {
309 maxScale = max;
310 superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
311 }
312
313 /**
314 * Get the min zoom multiplier.
315 * @return min zoom multiplier.
316 */
317 public float getMinZoom() {
318 return minScale;
319 }
320
321 /**
322 * Get the current zoom. This is the zoom relative to the initial
323 * scale, not the original resource.
324 * @return current zoom multiplier.
325 */
326 public float getCurrentZoom() {
327 return normalizedScale;
328 }
329
330 /**
331 * Set the min zoom multiplier. Default value: 1.
332 * @param min min zoom multiplier.
333 */
334 public void setMinZoom(float min) {
335 minScale = min;
336 superMinScale = SUPER_MIN_MULTIPLIER * minScale;
337 }
338
339 /**
340 * Reset zoom and translation to initial state.
341 */
342 public void resetZoom() {
343 normalizedScale = 1;
344 fitImageToView();
345 }
346
347 /**
348 * Set zoom to the specified scale. Image will be centered by default.
349 * @param scale
350 */
351 public void setZoom(float scale) {
352 setZoom(scale, 0.5f, 0.5f);
353 }
354
355 /**
356 * Set zoom to the specified scale. Image will be centered around the point
357 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
358 * as a fraction from the left and top of the view. For example, the top left
359 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
360 * @param scale
361 * @param focusX
362 * @param focusY
363 */
364 public void setZoom(float scale, float focusX, float focusY) {
365 setZoom(scale, focusX, focusY, mScaleType);
366 }
367
368 /**
369 * Set zoom to the specified scale. Image will be centered around the point
370 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
371 * as a fraction from the left and top of the view. For example, the top left
372 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
373 * @param scale
374 * @param focusX
375 * @param focusY
376 * @param scaleType
377 */
378 public void setZoom(float scale, float focusX, float focusY, ScaleType scaleType) {
379 //
380 // setZoom can be called before the image is on the screen, but at this point,
381 // image and view sizes have not yet been calculated in onMeasure. Thus, we should
382 // delay calling setZoom until the view has been measured.
383 //
384 if (!onDrawReady) {
385 delayedZoomVariables = new ZoomVariables(scale, focusX, focusY, scaleType);
386 return;
387 }
388
389 if (scaleType != mScaleType) {
390 setScaleType(scaleType);
391 }
392 resetZoom();
393 scaleImage(scale, viewWidth / 2, viewHeight / 2, true);
394 matrix.getValues(m);
395 m[Matrix.MTRANS_X] = -((focusX * getImageWidth()) - (viewWidth * 0.5f));
396 m[Matrix.MTRANS_Y] = -((focusY * getImageHeight()) - (viewHeight * 0.5f));
397 matrix.setValues(m);
398 fixTrans();
399 setImageMatrix(matrix);
400 }
401
402 /**
403 * Set zoom parameters equal to another TouchImageView. Including scale, position,
404 * and ScaleType.
405 * @param img
406 */
407 public void setZoom(TouchImageViewCustom img) {
408 PointF center = img.getScrollPosition();
409 setZoom(img.getCurrentZoom(), center.x, center.y, img.getScaleType());
410 }
411
412 /**
413 * Return the point at the center of the zoomed image. The PointF coordinates range
414 * in value between 0 and 1 and the focus point is denoted as a fraction from the left
415 * and top of the view. For example, the top left corner of the image would be (0, 0).
416 * And the bottom right corner would be (1, 1).
417 * @return PointF representing the scroll position of the zoomed image.
418 */
419 public PointF getScrollPosition() {
420 Drawable drawable = getDrawable();
421 if (drawable == null) {
422 return null;
423 }
424 int drawableWidth = drawable.getIntrinsicWidth();
425 int drawableHeight = drawable.getIntrinsicHeight();
426
427 PointF point = transformCoordTouchToBitmap(viewWidth / 2, viewHeight / 2, true);
428 point.x /= drawableWidth;
429 point.y /= drawableHeight;
430 return point;
431 }
432
433 /**
434 * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the
435 * left and top of the view. The focus points can range in value between 0 and 1.
436 * @param focusX
437 * @param focusY
438 */
439 public void setScrollPosition(float focusX, float focusY) {
440 setZoom(normalizedScale, focusX, focusY);
441 }
442
443 /**
444 * Performs boundary checking and fixes the image matrix if it
445 * is out of bounds.
446 */
447 private void fixTrans() {
448 matrix.getValues(m);
449 float transX = m[Matrix.MTRANS_X];
450 float transY = m[Matrix.MTRANS_Y];
451
452 float fixTransX = getFixTrans(transX, viewWidth, getImageWidth());
453 float fixTransY = getFixTrans(transY, viewHeight, getImageHeight());
454
455 if (fixTransX != 0 || fixTransY != 0) {
456 matrix.postTranslate(fixTransX, fixTransY);
457 }
458 }
459
460 /**
461 * When transitioning from zooming from focus to zoom from center (or vice versa)
462 * the image can become unaligned within the view. This is apparent when zooming
463 * quickly. When the content size is less than the view size, the content will often
464 * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
465 * then makes sure the image is centered correctly within the view.
466 */
467 private void fixScaleTrans() {
468 fixTrans();
469 matrix.getValues(m);
470 if (getImageWidth() < viewWidth) {
471 m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2;
472 }
473
474 if (getImageHeight() < viewHeight) {
475 m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2;
476 }
477 matrix.setValues(m);
478 }
479
480 private float getFixTrans(float trans, float viewSize, float contentSize) {
481 float minTrans, maxTrans;
482
483 if (contentSize <= viewSize) {
484 minTrans = 0;
485 maxTrans = viewSize - contentSize;
486
487 } else {
488 minTrans = viewSize - contentSize;
489 maxTrans = 0;
490 }
491
492 if (trans < minTrans)
493 return -trans + minTrans;
494 if (trans > maxTrans)
495 return -trans + maxTrans;
496 return 0;
497 }
498
499 private float getFixDragTrans(float delta, float viewSize, float contentSize) {
500 if (contentSize <= viewSize) {
501 return 0;
502 }
503 return delta;
504 }
505
506 private float getImageWidth() {
507 return matchViewWidth * normalizedScale;
508 }
509
510 private float getImageHeight() {
511 return matchViewHeight * normalizedScale;
512 }
513
514 @Override
515 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
516 Drawable drawable = getDrawable();
517 if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
518 setMeasuredDimension(0, 0);
519 return;
520 }
521
522 int drawableWidth = drawable.getIntrinsicWidth();
523 int drawableHeight = drawable.getIntrinsicHeight();
524 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
525 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
526 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
527 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
528 viewWidth = setViewSize(widthMode, widthSize, drawableWidth);
529 viewHeight = setViewSize(heightMode, heightSize, drawableHeight);
530
531 //
532 // Set view dimensions
533 //
534 setMeasuredDimension(viewWidth, viewHeight);
535
536 //
537 // Fit content within view
538 //
539 fitImageToView();
540 }
541
542 /**
543 * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
544 * it is made to fit the screen according to the dimensions of the previous image matrix. This
545 * allows the image to maintain its zoom after rotation.
546 */
547 private void fitImageToView() {
548 Drawable drawable = getDrawable();
549 if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
550 return;
551 }
552 if (matrix == null || prevMatrix == null) {
553 return;
554 }
555
556 int drawableWidth = drawable.getIntrinsicWidth();
557 int drawableHeight = drawable.getIntrinsicHeight();
558
559 //
560 // Scale image for view
561 //
562 float scaleX = (float) viewWidth / drawableWidth;
563 float scaleY = (float) viewHeight / drawableHeight;
564
565 switch (mScaleType) {
566 case CENTER:
567 scaleX = scaleY = 1;
568 break;
569
570 case CENTER_CROP:
571 scaleX = scaleY = Math.max(scaleX, scaleY);
572 break;
573
574 case CENTER_INSIDE:
575 scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY));
576
577 case FIT_CENTER:
578 scaleX = scaleY = Math.min(scaleX, scaleY);
579 break;
580
581 case FIT_XY:
582 break;
583
584 default:
585 //
586 // FIT_START and FIT_END not supported
587 //
588 throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
589
590 }
591
592 //
593 // Center the image
594 //
595 float redundantXSpace = viewWidth - (scaleX * drawableWidth);
596 float redundantYSpace = viewHeight - (scaleY * drawableHeight);
597 matchViewWidth = viewWidth - redundantXSpace;
598 matchViewHeight = viewHeight - redundantYSpace;
599 if (!isZoomed() && !imageRenderedAtLeastOnce) {
600 //
601 // Stretch and center image to fit view
602 //
603 matrix.setScale(scaleX, scaleY);
604 matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2);
605 normalizedScale = 1;
606
607 } else {
608 //
609 // These values should never be 0 or we will set viewWidth and viewHeight
610 // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues
611 // to set them equal to the current values.
612 //
613 if (prevMatchViewWidth == 0 || prevMatchViewHeight == 0) {
614 savePreviousImageValues();
615 }
616
617 prevMatrix.getValues(m);
618
619 //
620 // Rescale Matrix after rotation
621 //
622 m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale;
623 m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale;
624
625 //
626 // TransX and TransY from previous matrix
627 //
628 float transX = m[Matrix.MTRANS_X];
629 float transY = m[Matrix.MTRANS_Y];
630
631 //
632 // Width
633 //
634 float prevActualWidth = prevMatchViewWidth * normalizedScale;
635 float actualWidth = getImageWidth();
636 translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth);
637
638 //
639 // Height
640 //
641 float prevActualHeight = prevMatchViewHeight * normalizedScale;
642 float actualHeight = getImageHeight();
643 translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight);
644
645 //
646 // Set the matrix to the adjusted scale and translate values.
647 //
648 matrix.setValues(m);
649 }
650 fixTrans();
651 setImageMatrix(matrix);
652 }
653
654 /**
655 * Set view dimensions based on layout params
656 *
657 * @param mode
658 * @param size
659 * @param drawableWidth
660 * @return
661 */
662 private int setViewSize(int mode, int size, int drawableWidth) {
663 int viewSize;
664 switch (mode) {
665 case MeasureSpec.EXACTLY:
666 viewSize = size;
667 break;
668
669 case MeasureSpec.AT_MOST:
670 viewSize = Math.min(drawableWidth, size);
671 break;
672
673 case MeasureSpec.UNSPECIFIED:
674 viewSize = drawableWidth;
675 break;
676
677 default:
678 viewSize = size;
679 break;
680 }
681 return viewSize;
682 }
683
684 /**
685 * After rotating, the matrix needs to be translated. This function finds the area of image
686 * which was previously centered and adjusts translations so that is again the center, post-rotation.
687 *
688 * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y
689 * @param trans the value of trans in that axis before the rotation
690 * @param prevImageSize the width/height of the image before the rotation
691 * @param imageSize width/height of the image after rotation
692 * @param prevViewSize width/height of view before rotation
693 * @param viewSize width/height of view after rotation
694 * @param drawableSize width/height of drawable
695 */
696 private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) {
697 if (imageSize < viewSize) {
698 //
699 // The width/height of image is less than the view's width/height. Center it.
700 //
701 m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f;
702
703 } else if (trans > 0) {
704 //
705 // The image is larger than the view, but was not before rotation. Center it.
706 //
707 m[axis] = -((imageSize - viewSize) * 0.5f);
708
709 } else {
710 //
711 // Find the area of the image which was previously centered in the view. Determine its distance
712 // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage
713 // to calculate the trans in the new view width/height.
714 //
715 float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize;
716 m[axis] = -((percentage * imageSize) - (viewSize * 0.5f));
717 }
718 }
719
720 private void setState(State state) {
721 this.state = state;
722 }
723
724 public boolean canScrollHorizontallyFroyo(int direction) {
725 return canScrollHorizontally(direction);
726 }
727
728 @Override
729 public boolean canScrollHorizontally(int direction) {
730 matrix.getValues(m);
731 float x = m[Matrix.MTRANS_X];
732
733 if (getImageWidth() < viewWidth) {
734 return false;
735
736 } else if (x >= -1 && direction < 0) {
737 return false;
738
739 } else if (Math.abs(x) + viewWidth + 1 >= getImageWidth() && direction > 0) {
740 return false;
741 }
742
743 return true;
744 }
745
746 /**
747 * Gesture Listener detects a single click or long click and passes that on
748 * to the view's listener.
749 * @author Ortiz
750 *
751 */
752 private class GestureListener extends GestureDetector.SimpleOnGestureListener {
753
754 @Override
755 public boolean onSingleTapConfirmed(MotionEvent e)
756 {
757 if(doubleTapListener != null) {
758 return doubleTapListener.onSingleTapConfirmed(e);
759 }
760 return performClick();
761 }
762
763 @Override
764 public void onLongPress(MotionEvent e)
765 {
766 performLongClick();
767 }
768
769 @Override
770 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
771 {
772 if (fling != null) {
773 //
774 // If a previous fling is still active, it should be cancelled so that two flings
775 // are not run simultaenously.
776 //
777 fling.cancelFling();
778 }
779 fling = new Fling((int) velocityX, (int) velocityY);
780 compatPostOnAnimation(fling);
781 return super.onFling(e1, e2, velocityX, velocityY);
782 }
783
784 @Override
785 public boolean onDoubleTap(MotionEvent e) {
786 boolean consumed = false;
787 if(doubleTapListener != null) {
788 consumed = doubleTapListener.onDoubleTap(e);
789 }
790 if (state == State.NONE) {
791 float targetZoom = (normalizedScale == minScale) ? maxScale : minScale;
792 DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false);
793 compatPostOnAnimation(doubleTap);
794 consumed = true;
795 }
796 return consumed;
797 }
798
799 @Override
800 public boolean onDoubleTapEvent(MotionEvent e) {
801 if(doubleTapListener != null) {
802 return doubleTapListener.onDoubleTapEvent(e);
803 }
804 return false;
805 }
806 }
807
808 public interface OnTouchImageViewListener {
809 public void onMove();
810 }
811
812 /**
813 * Responsible for all touch events. Handles the heavy lifting of drag and also sends
814 * touch events to Scale Detector and Gesture Detector.
815 * @author Ortiz
816 *
817 */
818 private class PrivateOnTouchListener implements OnTouchListener {
819
820 //
821 // Remember last point position for dragging
822 //
823 private PointF last = new PointF();
824
825 @Override
826 public boolean onTouch(View v, MotionEvent event) {
827 mScaleDetector.onTouchEvent(event);
828 mGestureDetector.onTouchEvent(event);
829 PointF curr = new PointF(event.getX(), event.getY());
830
831 if (state == State.NONE || state == State.DRAG || state == State.FLING) {
832 switch (event.getAction()) {
833 case MotionEvent.ACTION_DOWN:
834 last.set(curr);
835 if (fling != null)
836 fling.cancelFling();
837 setState(State.DRAG);
838 break;
839
840 case MotionEvent.ACTION_MOVE:
841 if (state == State.DRAG) {
842 float deltaX = curr.x - last.x;
843 float deltaY = curr.y - last.y;
844 float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth());
845 float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight());
846 matrix.postTranslate(fixTransX, fixTransY);
847 fixTrans();
848 last.set(curr.x, curr.y);
849 }
850 break;
851
852 case MotionEvent.ACTION_UP:
853 case MotionEvent.ACTION_POINTER_UP:
854 setState(State.NONE);
855 break;
856 }
857 }
858
859 setImageMatrix(matrix);
860
861 //
862 // User-defined OnTouchListener
863 //
864 if(userTouchListener != null) {
865 userTouchListener.onTouch(v, event);
866 }
867
868 //
869 // OnTouchImageViewListener is set: TouchImageView dragged by user.
870 //
871 if (touchImageViewListener != null) {
872 touchImageViewListener.onMove();
873 }
874
875 //
876 // indicate event was handled
877 //
878 return true;
879 }
880 }
881
882 /**
883 * ScaleListener detects user two finger scaling and scales image.
884 * @author Ortiz
885 *
886 */
887 private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
888 @Override
889 public boolean onScaleBegin(ScaleGestureDetector detector) {
890 setState(State.ZOOM);
891 return true;
892 }
893
894 @Override
895 public boolean onScale(ScaleGestureDetector detector) {
896 scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true);
897
898 //
899 // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
900 //
901 if (touchImageViewListener != null) {
902 touchImageViewListener.onMove();
903 }
904 return true;
905 }
906
907 @Override
908 public void onScaleEnd(ScaleGestureDetector detector) {
909 super.onScaleEnd(detector);
910 setState(State.NONE);
911 boolean animateToZoomBoundary = false;
912 float targetZoom = normalizedScale;
913 if (normalizedScale > maxScale) {
914 targetZoom = maxScale;
915 animateToZoomBoundary = true;
916
917 } else if (normalizedScale < minScale) {
918 targetZoom = minScale;
919 animateToZoomBoundary = true;
920 }
921
922 if (animateToZoomBoundary) {
923 DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true);
924 compatPostOnAnimation(doubleTap);
925 }
926 }
927 }
928
929 private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) {
930
931 float lowerScale, upperScale;
932 if (stretchImageToSuper) {
933 lowerScale = superMinScale;
934 upperScale = superMaxScale;
935
936 } else {
937 lowerScale = minScale;
938 upperScale = maxScale;
939 }
940
941 float origScale = normalizedScale;
942 normalizedScale *= deltaScale;
943 if (normalizedScale > upperScale) {
944 normalizedScale = upperScale;
945 deltaScale = upperScale / origScale;
946 } else if (normalizedScale < lowerScale) {
947 normalizedScale = lowerScale;
948 deltaScale = lowerScale / origScale;
949 }
950
951 matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY);
952 fixScaleTrans();
953 }
954
955 /**
956 * DoubleTapZoom calls a series of runnables which apply
957 * an animated zoom in/out graphic to the image.
958 * @author Ortiz
959 *
960 */
961 private class DoubleTapZoom implements Runnable {
962
963 private long startTime;
964 private static final float ZOOM_TIME = 500;
965 private float startZoom, targetZoom;
966 private float bitmapX, bitmapY;
967 private boolean stretchImageToSuper;
968 private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
969 private PointF startTouch;
970 private PointF endTouch;
971
972 DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) {
973 setState(State.ANIMATE_ZOOM);
974 startTime = System.currentTimeMillis();
975 this.startZoom = normalizedScale;
976 this.targetZoom = targetZoom;
977 this.stretchImageToSuper = stretchImageToSuper;
978 PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false);
979 this.bitmapX = bitmapPoint.x;
980 this.bitmapY = bitmapPoint.y;
981
982 //
983 // Used for translating image during scaling
984 //
985 startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY);
986 endTouch = new PointF(viewWidth / 2, viewHeight / 2);
987 }
988
989 @Override
990 public void run() {
991 float t = interpolate();
992 double deltaScale = calculateDeltaScale(t);
993 scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper);
994 translateImageToCenterTouchPosition(t);
995 fixScaleTrans();
996 setImageMatrix(matrix);
997
998 //
999 // OnTouchImageViewListener is set: double tap runnable updates listener
1000 // with every frame.
1001 //
1002 if (touchImageViewListener != null) {
1003 touchImageViewListener.onMove();
1004 }
1005
1006 if (t < 1f) {
1007 //
1008 // We haven't finished zooming
1009 //
1010 compatPostOnAnimation(this);
1011
1012 } else {
1013 //
1014 // Finished zooming
1015 //
1016 setState(State.NONE);
1017 }
1018 }
1019
1020 /**
1021 * Interpolate between where the image should start and end in order to translate
1022 * the image so that the point that is touched is what ends up centered at the end
1023 * of the zoom.
1024 * @param t
1025 */
1026 private void translateImageToCenterTouchPosition(float t) {
1027 float targetX = startTouch.x + t * (endTouch.x - startTouch.x);
1028 float targetY = startTouch.y + t * (endTouch.y - startTouch.y);
1029 PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY);
1030 matrix.postTranslate(targetX - curr.x, targetY - curr.y);
1031 }
1032
1033 /**
1034 * Use interpolator to get t
1035 * @return
1036 */
1037 private float interpolate() {
1038 long currTime = System.currentTimeMillis();
1039 float elapsed = (currTime - startTime) / ZOOM_TIME;
1040 elapsed = Math.min(1f, elapsed);
1041 return interpolator.getInterpolation(elapsed);
1042 }
1043
1044 /**
1045 * Interpolate the current targeted zoom and get the delta
1046 * from the current zoom.
1047 * @param t
1048 * @return
1049 */
1050 private double calculateDeltaScale(float t) {
1051 double zoom = startZoom + t * (targetZoom - startZoom);
1052 return zoom / normalizedScale;
1053 }
1054 }
1055
1056 /**
1057 * This function will transform the coordinates in the touch event to the coordinate
1058 * system of the drawable that the imageview contain
1059 * @param x x-coordinate of touch event
1060 * @param y y-coordinate of touch event
1061 * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
1062 * to the bounds of the bitmap size.
1063 * @return Coordinates of the point touched, in the coordinate system of the original drawable.
1064 */
1065 private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) {
1066 matrix.getValues(m);
1067 float origW = getDrawable().getIntrinsicWidth();
1068 float origH = getDrawable().getIntrinsicHeight();
1069 float transX = m[Matrix.MTRANS_X];
1070 float transY = m[Matrix.MTRANS_Y];
1071 float finalX = ((x - transX) * origW) / getImageWidth();
1072 float finalY = ((y - transY) * origH) / getImageHeight();
1073
1074 if (clipToBitmap) {
1075 finalX = Math.min(Math.max(finalX, 0), origW);
1076 finalY = Math.min(Math.max(finalY, 0), origH);
1077 }
1078
1079 return new PointF(finalX , finalY);
1080 }
1081
1082 /**
1083 * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
1084 * drawable's coordinate system to the view's coordinate system.
1085 * @param bx x-coordinate in original bitmap coordinate system
1086 * @param by y-coordinate in original bitmap coordinate system
1087 * @return Coordinates of the point in the view's coordinate system.
1088 */
1089 private PointF transformCoordBitmapToTouch(float bx, float by) {
1090 matrix.getValues(m);
1091 float origW = getDrawable().getIntrinsicWidth();
1092 float origH = getDrawable().getIntrinsicHeight();
1093 float px = bx / origW;
1094 float py = by / origH;
1095 float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px;
1096 float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py;
1097 return new PointF(finalX , finalY);
1098 }
1099
1100 /**
1101 * Fling launches sequential runnables which apply
1102 * the fling graphic to the image. The values for the translation
1103 * are interpolated by the Scroller.
1104 * @author Ortiz
1105 *
1106 */
1107 private class Fling implements Runnable {
1108
1109 CompatScroller scroller;
1110 int currX, currY;
1111
1112 Fling(int velocityX, int velocityY) {
1113 setState(State.FLING);
1114 scroller = new CompatScroller(context);
1115 matrix.getValues(m);
1116
1117 int startX = (int) m[Matrix.MTRANS_X];
1118 int startY = (int) m[Matrix.MTRANS_Y];
1119 int minX, maxX, minY, maxY;
1120
1121 if (getImageWidth() > viewWidth) {
1122 minX = viewWidth - (int) getImageWidth();
1123 maxX = 0;
1124
1125 } else {
1126 minX = maxX = startX;
1127 }
1128
1129 if (getImageHeight() > viewHeight) {
1130 minY = viewHeight - (int) getImageHeight();
1131 maxY = 0;
1132
1133 } else {
1134 minY = maxY = startY;
1135 }
1136
1137 scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX,
1138 maxX, minY, maxY);
1139 currX = startX;
1140 currY = startY;
1141 }
1142
1143 public void cancelFling() {
1144 if (scroller != null) {
1145 setState(State.NONE);
1146 scroller.forceFinished(true);
1147 }
1148 }
1149
1150 @Override
1151 public void run() {
1152
1153 //
1154 // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
1155 // Listener runnable updated with each frame of fling animation.
1156 //
1157 if (touchImageViewListener != null) {
1158 touchImageViewListener.onMove();
1159 }
1160
1161 if (scroller.isFinished()) {
1162 scroller = null;
1163 return;
1164 }
1165
1166 if (scroller.computeScrollOffset()) {
1167 int newX = scroller.getCurrX();
1168 int newY = scroller.getCurrY();
1169 int transX = newX - currX;
1170 int transY = newY - currY;
1171 currX = newX;
1172 currY = newY;
1173 matrix.postTranslate(transX, transY);
1174 fixTrans();
1175 setImageMatrix(matrix);
1176 compatPostOnAnimation(this);
1177 }
1178 }
1179 }
1180
1181 @TargetApi(Build.VERSION_CODES.GINGERBREAD)
1182 private class CompatScroller {
1183 Scroller scroller;
1184 OverScroller overScroller;
1185 boolean isPreGingerbread;
1186
1187 public CompatScroller(Context context) {
1188 if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
1189 isPreGingerbread = true;
1190 scroller = new Scroller(context);
1191
1192 } else {
1193 isPreGingerbread = false;
1194 overScroller = new OverScroller(context);
1195 }
1196 }
1197
1198 public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
1199 if (isPreGingerbread) {
1200 scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
1201 } else {
1202 overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
1203 }
1204 }
1205
1206 public void forceFinished(boolean finished) {
1207 if (isPreGingerbread) {
1208 scroller.forceFinished(finished);
1209 } else {
1210 overScroller.forceFinished(finished);
1211 }
1212 }
1213
1214 public boolean isFinished() {
1215 if (isPreGingerbread) {
1216 return scroller.isFinished();
1217 } else {
1218 return overScroller.isFinished();
1219 }
1220 }
1221
1222 public boolean computeScrollOffset() {
1223 if (isPreGingerbread) {
1224 return scroller.computeScrollOffset();
1225 } else {
1226 overScroller.computeScrollOffset();
1227 return overScroller.computeScrollOffset();
1228 }
1229 }
1230
1231 public int getCurrX() {
1232 if (isPreGingerbread) {
1233 return scroller.getCurrX();
1234 } else {
1235 return overScroller.getCurrX();
1236 }
1237 }
1238
1239 public int getCurrY() {
1240 if (isPreGingerbread) {
1241 return scroller.getCurrY();
1242 } else {
1243 return overScroller.getCurrY();
1244 }
1245 }
1246 }
1247
1248 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
1249 private void compatPostOnAnimation(Runnable runnable) {
1250 if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
1251 postOnAnimation(runnable);
1252
1253 } else {
1254 postDelayed(runnable, 1000/60);
1255 }
1256 }
1257
1258 private class ZoomVariables {
1259 public float scale;
1260 public float focusX;
1261 public float focusY;
1262 public ScaleType scaleType;
1263
1264 public ZoomVariables(float scale, float focusX, float focusY, ScaleType scaleType) {
1265 this.scale = scale;
1266 this.focusX = focusX;
1267 this.focusY = focusY;
1268 this.scaleType = scaleType;
1269 }
1270 }
1271
1272 private void printMatrixInfo() {
1273 float[] n = new float[9];
1274 matrix.getValues(n);
1275 Log.d(DEBUG, "Scale: " + n[Matrix.MSCALE_X] + " TransX: " + n[Matrix.MTRANS_X] + " TransY: " + n[Matrix.MTRANS_Y]);
1276 }
1277 }