TouchImageView.java 41 KB

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