TouchImageViewCustom.java 41 KB

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