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