TouchImageViewCustom.java 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280
  1. /**
  2. * @author Michael Ortiz
  3. * @updated Patrick Lackemacher
  4. * @updated Babay88
  5. * @updated @ipsilondev
  6. * @updated hank-cp
  7. * @updated singpolyma
  8. * Copyright (c) 2012 Michael Ortiz
  9. */
  10. package third_parties.michaelOrtiz;
  11. import android.annotation.TargetApi;
  12. import android.content.Context;
  13. import android.content.res.Configuration;
  14. import android.graphics.Bitmap;
  15. import android.graphics.Canvas;
  16. import android.graphics.Matrix;
  17. import android.graphics.PointF;
  18. import android.graphics.RectF;
  19. import android.graphics.drawable.Drawable;
  20. import android.net.Uri;
  21. import android.os.Build;
  22. import android.os.Build.VERSION;
  23. import android.os.Build.VERSION_CODES;
  24. import android.os.Bundle;
  25. import android.os.Parcelable;
  26. import android.util.AttributeSet;
  27. import android.util.Log;
  28. import android.view.GestureDetector;
  29. import android.view.MotionEvent;
  30. import android.view.ScaleGestureDetector;
  31. import android.view.View;
  32. import android.view.animation.AccelerateDecelerateInterpolator;
  33. import android.widget.OverScroller;
  34. import android.widget.Scroller;
  35. import com.owncloud.android.ui.preview.ImageViewCustom;
  36. /**
  37. * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom.
  38. */
  39. public class TouchImageViewCustom extends ImageViewCustom {
  40. private static final String DEBUG = "DEBUG";
  41. //
  42. // SuperMin and SuperMax multipliers. Determine how much the image can be
  43. // zoomed below or above the zoom boundaries, before animating back to the
  44. // min/max zoom boundary.
  45. //
  46. private static final float SUPER_MIN_MULTIPLIER = .75f;
  47. private static final float SUPER_MAX_MULTIPLIER = 1.25f;
  48. //
  49. // Scale of image ranges from minScale to maxScale, where minScale == 1
  50. // when the image is stretched to fit view.
  51. //
  52. private float normalizedScale;
  53. //
  54. // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
  55. // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
  56. // saved prior to the screen rotating.
  57. //
  58. private Matrix matrix, prevMatrix;
  59. private static 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 TouchImageViewCustom(Context context) {
  86. super(context);
  87. sharedConstructing(context);
  88. }
  89. public TouchImageViewCustom(Context context, AttributeSet attrs) {
  90. super(context, attrs);
  91. sharedConstructing(context);
  92. }
  93. public TouchImageViewCustom(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.equals(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(TouchImageViewCustom 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, maxTrans;
  423. if (contentSize <= viewSize) {
  424. minTrans = 0;
  425. maxTrans = viewSize - contentSize;
  426. } else {
  427. minTrans = viewSize - contentSize;
  428. maxTrans = 0;
  429. }
  430. if (trans < minTrans) {
  431. return -trans + minTrans;
  432. }
  433. if (trans > maxTrans) {
  434. return -trans + maxTrans;
  435. }
  436. return 0;
  437. }
  438. private float getFixDragTrans(float delta, float viewSize, float contentSize) {
  439. if (contentSize <= viewSize) {
  440. return 0;
  441. }
  442. return delta;
  443. }
  444. private float getImageWidth() {
  445. return matchViewWidth * normalizedScale;
  446. }
  447. private float getImageHeight() {
  448. return matchViewHeight * normalizedScale;
  449. }
  450. @Override
  451. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  452. Drawable drawable = getDrawable();
  453. if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
  454. setMeasuredDimension(0, 0);
  455. return;
  456. }
  457. int drawableWidth = drawable.getIntrinsicWidth();
  458. int drawableHeight = drawable.getIntrinsicHeight();
  459. int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  460. int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  461. int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  462. int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  463. viewWidth = setViewSize(widthMode, widthSize, drawableWidth);
  464. viewHeight = setViewSize(heightMode, heightSize, drawableHeight);
  465. //
  466. // Set view dimensions
  467. //
  468. setMeasuredDimension(viewWidth, viewHeight);
  469. //
  470. // Fit content within view
  471. //
  472. fitImageToView();
  473. }
  474. /**
  475. * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
  476. * it is made to fit the screen according to the dimensions of the previous image matrix. This
  477. * allows the image to maintain its zoom after rotation.
  478. */
  479. private void fitImageToView() {
  480. Drawable drawable = getDrawable();
  481. if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
  482. return;
  483. }
  484. if (matrix == null || prevMatrix == null) {
  485. return;
  486. }
  487. int drawableWidth = drawable.getIntrinsicWidth();
  488. int drawableHeight = drawable.getIntrinsicHeight();
  489. //
  490. // Scale image for view
  491. //
  492. float scaleX = (float) viewWidth / drawableWidth;
  493. float scaleY = (float) viewHeight / drawableHeight;
  494. switch (mScaleType) {
  495. case CENTER:
  496. scaleX = scaleY = 1;
  497. break;
  498. case CENTER_CROP:
  499. scaleX = scaleY = Math.max(scaleX, scaleY);
  500. break;
  501. case CENTER_INSIDE:
  502. scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY));
  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. }
  728. setState(State.DRAG);
  729. break;
  730. case MotionEvent.ACTION_MOVE:
  731. if (state == State.DRAG) {
  732. float deltaX = curr.x - last.x;
  733. float deltaY = curr.y - last.y;
  734. float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth());
  735. float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight());
  736. matrix.postTranslate(fixTransX, fixTransY);
  737. fixTrans();
  738. last.set(curr.x, curr.y);
  739. }
  740. break;
  741. case MotionEvent.ACTION_UP:
  742. case MotionEvent.ACTION_POINTER_UP:
  743. setState(State.NONE);
  744. break;
  745. }
  746. }
  747. setImageMatrix(matrix);
  748. //
  749. // User-defined OnTouchListener
  750. //
  751. if(userTouchListener != null) {
  752. userTouchListener.onTouch(v, event);
  753. }
  754. //
  755. // OnTouchImageViewListener is set: TouchImageView dragged by user.
  756. //
  757. if (touchImageViewListener != null) {
  758. touchImageViewListener.onMove();
  759. }
  760. //
  761. // indicate event was handled
  762. //
  763. return true;
  764. }
  765. }
  766. /**
  767. * ScaleListener detects user two finger scaling and scales image.
  768. * @author Ortiz
  769. *
  770. */
  771. private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
  772. @Override
  773. public boolean onScaleBegin(ScaleGestureDetector detector) {
  774. setState(State.ZOOM);
  775. return true;
  776. }
  777. @Override
  778. public boolean onScale(ScaleGestureDetector detector) {
  779. scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true);
  780. //
  781. // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
  782. //
  783. if (touchImageViewListener != null) {
  784. touchImageViewListener.onMove();
  785. }
  786. return true;
  787. }
  788. @Override
  789. public void onScaleEnd(ScaleGestureDetector detector) {
  790. super.onScaleEnd(detector);
  791. setState(State.NONE);
  792. boolean animateToZoomBoundary = false;
  793. float targetZoom = normalizedScale;
  794. if (normalizedScale > maxScale) {
  795. targetZoom = maxScale;
  796. animateToZoomBoundary = true;
  797. } else if (normalizedScale < minScale) {
  798. targetZoom = minScale;
  799. animateToZoomBoundary = true;
  800. }
  801. if (animateToZoomBoundary) {
  802. DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true);
  803. compatPostOnAnimation(doubleTap);
  804. }
  805. }
  806. }
  807. private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) {
  808. float lowerScale, upperScale;
  809. if (stretchImageToSuper) {
  810. lowerScale = superMinScale;
  811. upperScale = superMaxScale;
  812. } else {
  813. lowerScale = minScale;
  814. upperScale = maxScale;
  815. }
  816. float origScale = normalizedScale;
  817. normalizedScale *= deltaScale;
  818. if (normalizedScale > upperScale) {
  819. normalizedScale = upperScale;
  820. deltaScale = upperScale / origScale;
  821. } else if (normalizedScale < lowerScale) {
  822. normalizedScale = lowerScale;
  823. deltaScale = lowerScale / origScale;
  824. }
  825. matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY);
  826. fixScaleTrans();
  827. }
  828. /**
  829. * DoubleTapZoom calls a series of runnables which apply
  830. * an animated zoom in/out graphic to the image.
  831. * @author Ortiz
  832. *
  833. */
  834. private class DoubleTapZoom implements Runnable {
  835. private long startTime;
  836. private static final float ZOOM_TIME = 500;
  837. private float startZoom, targetZoom;
  838. private float bitmapX, bitmapY;
  839. private boolean stretchImageToSuper;
  840. private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
  841. private PointF startTouch;
  842. private PointF endTouch;
  843. DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) {
  844. setState(State.ANIMATE_ZOOM);
  845. startTime = System.currentTimeMillis();
  846. this.startZoom = normalizedScale;
  847. this.targetZoom = targetZoom;
  848. this.stretchImageToSuper = stretchImageToSuper;
  849. PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false);
  850. this.bitmapX = bitmapPoint.x;
  851. this.bitmapY = bitmapPoint.y;
  852. //
  853. // Used for translating image during scaling
  854. //
  855. startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY);
  856. endTouch = new PointF(viewWidth / 2, viewHeight / 2);
  857. }
  858. @Override
  859. public void run() {
  860. float t = interpolate();
  861. double deltaScale = calculateDeltaScale(t);
  862. scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper);
  863. translateImageToCenterTouchPosition(t);
  864. fixScaleTrans();
  865. setImageMatrix(matrix);
  866. //
  867. // OnTouchImageViewListener is set: double tap runnable updates listener
  868. // with every frame.
  869. //
  870. if (touchImageViewListener != null) {
  871. touchImageViewListener.onMove();
  872. }
  873. if (t < 1f) {
  874. //
  875. // We haven't finished zooming
  876. //
  877. compatPostOnAnimation(this);
  878. } else {
  879. //
  880. // Finished zooming
  881. //
  882. setState(State.NONE);
  883. }
  884. }
  885. /**
  886. * Interpolate between where the image should start and end in order to translate
  887. * the image so that the point that is touched is what ends up centered at the end
  888. * of the zoom.
  889. * @param t
  890. */
  891. private void translateImageToCenterTouchPosition(float t) {
  892. float targetX = startTouch.x + t * (endTouch.x - startTouch.x);
  893. float targetY = startTouch.y + t * (endTouch.y - startTouch.y);
  894. PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY);
  895. matrix.postTranslate(targetX - curr.x, targetY - curr.y);
  896. }
  897. /**
  898. * Use interpolator to get t
  899. * @return
  900. */
  901. private float interpolate() {
  902. long currTime = System.currentTimeMillis();
  903. float elapsed = (currTime - startTime) / ZOOM_TIME;
  904. elapsed = Math.min(1f, elapsed);
  905. return interpolator.getInterpolation(elapsed);
  906. }
  907. /**
  908. * Interpolate the current targeted zoom and get the delta
  909. * from the current zoom.
  910. * @param t
  911. * @return
  912. */
  913. private double calculateDeltaScale(float t) {
  914. double zoom = startZoom + t * (targetZoom - startZoom);
  915. return zoom / normalizedScale;
  916. }
  917. }
  918. /**
  919. * This function will transform the coordinates in the touch event to the coordinate
  920. * system of the drawable that the imageview contain
  921. * @param x x-coordinate of touch event
  922. * @param y y-coordinate of touch event
  923. * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
  924. * to the bounds of the bitmap size.
  925. * @return Coordinates of the point touched, in the coordinate system of the original drawable.
  926. */
  927. private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) {
  928. matrix.getValues(m);
  929. float origW = getDrawable().getIntrinsicWidth();
  930. float origH = getDrawable().getIntrinsicHeight();
  931. float transX = m[Matrix.MTRANS_X];
  932. float transY = m[Matrix.MTRANS_Y];
  933. float finalX = ((x - transX) * origW) / getImageWidth();
  934. float finalY = ((y - transY) * origH) / getImageHeight();
  935. if (clipToBitmap) {
  936. finalX = Math.min(Math.max(finalX, 0), origW);
  937. finalY = Math.min(Math.max(finalY, 0), origH);
  938. }
  939. return new PointF(finalX , finalY);
  940. }
  941. /**
  942. * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
  943. * drawable's coordinate system to the view's coordinate system.
  944. * @param bx x-coordinate in original bitmap coordinate system
  945. * @param by y-coordinate in original bitmap coordinate system
  946. * @return Coordinates of the point in the view's coordinate system.
  947. */
  948. private PointF transformCoordBitmapToTouch(float bx, float by) {
  949. matrix.getValues(m);
  950. float origW = getDrawable().getIntrinsicWidth();
  951. float origH = getDrawable().getIntrinsicHeight();
  952. float px = bx / origW;
  953. float py = by / origH;
  954. float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px;
  955. float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py;
  956. return new PointF(finalX , finalY);
  957. }
  958. /**
  959. * Fling launches sequential runnables which apply
  960. * the fling graphic to the image. The values for the translation
  961. * are interpolated by the Scroller.
  962. * @author Ortiz
  963. *
  964. */
  965. private class Fling implements Runnable {
  966. CompatScroller scroller;
  967. int currX, currY;
  968. Fling(int velocityX, int velocityY) {
  969. setState(State.FLING);
  970. scroller = new CompatScroller(context);
  971. matrix.getValues(m);
  972. int startX = (int) m[Matrix.MTRANS_X];
  973. int startY = (int) m[Matrix.MTRANS_Y];
  974. int minX, maxX, minY, maxY;
  975. if (getImageWidth() > viewWidth) {
  976. minX = viewWidth - (int) getImageWidth();
  977. maxX = 0;
  978. } else {
  979. minX = maxX = startX;
  980. }
  981. if (getImageHeight() > viewHeight) {
  982. minY = viewHeight - (int) getImageHeight();
  983. maxY = 0;
  984. } else {
  985. minY = maxY = startY;
  986. }
  987. scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX,
  988. maxX, minY, maxY);
  989. currX = startX;
  990. currY = startY;
  991. }
  992. public void cancelFling() {
  993. if (scroller != null) {
  994. setState(State.NONE);
  995. scroller.forceFinished(true);
  996. }
  997. }
  998. @Override
  999. public void run() {
  1000. //
  1001. // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
  1002. // Listener runnable updated with each frame of fling animation.
  1003. //
  1004. if (touchImageViewListener != null) {
  1005. touchImageViewListener.onMove();
  1006. }
  1007. if (scroller.isFinished()) {
  1008. scroller = null;
  1009. return;
  1010. }
  1011. if (scroller.computeScrollOffset()) {
  1012. int newX = scroller.getCurrX();
  1013. int newY = scroller.getCurrY();
  1014. int transX = newX - currX;
  1015. int transY = newY - currY;
  1016. currX = newX;
  1017. currY = newY;
  1018. matrix.postTranslate(transX, transY);
  1019. fixTrans();
  1020. setImageMatrix(matrix);
  1021. compatPostOnAnimation(this);
  1022. }
  1023. }
  1024. }
  1025. @TargetApi(Build.VERSION_CODES.GINGERBREAD)
  1026. private class CompatScroller {
  1027. Scroller scroller;
  1028. OverScroller overScroller;
  1029. boolean isPreGingerbread;
  1030. public CompatScroller(Context context) {
  1031. if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
  1032. isPreGingerbread = true;
  1033. scroller = new Scroller(context);
  1034. } else {
  1035. isPreGingerbread = false;
  1036. overScroller = new OverScroller(context);
  1037. }
  1038. }
  1039. public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
  1040. if (isPreGingerbread) {
  1041. scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
  1042. } else {
  1043. overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
  1044. }
  1045. }
  1046. public void forceFinished(boolean finished) {
  1047. if (isPreGingerbread) {
  1048. scroller.forceFinished(finished);
  1049. } else {
  1050. overScroller.forceFinished(finished);
  1051. }
  1052. }
  1053. public boolean isFinished() {
  1054. if (isPreGingerbread) {
  1055. return scroller.isFinished();
  1056. } else {
  1057. return overScroller.isFinished();
  1058. }
  1059. }
  1060. public boolean computeScrollOffset() {
  1061. if (isPreGingerbread) {
  1062. return scroller.computeScrollOffset();
  1063. } else {
  1064. overScroller.computeScrollOffset();
  1065. return overScroller.computeScrollOffset();
  1066. }
  1067. }
  1068. public int getCurrX() {
  1069. if (isPreGingerbread) {
  1070. return scroller.getCurrX();
  1071. } else {
  1072. return overScroller.getCurrX();
  1073. }
  1074. }
  1075. public int getCurrY() {
  1076. if (isPreGingerbread) {
  1077. return scroller.getCurrY();
  1078. } else {
  1079. return overScroller.getCurrY();
  1080. }
  1081. }
  1082. }
  1083. @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
  1084. private void compatPostOnAnimation(Runnable runnable) {
  1085. if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
  1086. postOnAnimation(runnable);
  1087. } else {
  1088. postDelayed(runnable, 1000/60);
  1089. }
  1090. }
  1091. private class ZoomVariables {
  1092. public float scale;
  1093. public float focusX;
  1094. public float focusY;
  1095. public ScaleType scaleType;
  1096. public ZoomVariables(float scale, float focusX, float focusY, ScaleType scaleType) {
  1097. this.scale = scale;
  1098. this.focusX = focusX;
  1099. this.focusY = focusY;
  1100. this.scaleType = scaleType;
  1101. }
  1102. }
  1103. private void printMatrixInfo() {
  1104. float[] n = new float[9];
  1105. matrix.getValues(n);
  1106. Log.d(DEBUG, "Scale: " + n[Matrix.MSCALE_X] + " TransX: " + n[Matrix.MTRANS_X] + " TransY: " + n[Matrix.MTRANS_Y]);
  1107. }
  1108. }