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