Advertisement
Guest User

Untitled

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