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