TouchImageView.java 41 KB

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