Created
November 22, 2016 10:08
-
-
Save vinaysshenoy/c03be9892676c2e02fc729ff243eaeff to your computer and use it in GitHub Desktop.
Gist to demonstrate using Matrixes to handle gestures for child elements in a View
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.vinaysshenoy.matrixviewtest; | |
import android.annotation.TargetApi; | |
import android.content.Context; | |
import android.content.res.Resources; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.Matrix; | |
import android.graphics.Paint; | |
import android.graphics.PointF; | |
import android.graphics.Rect; | |
import android.graphics.RectF; | |
import android.os.Build; | |
import android.support.annotation.IntDef; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.MotionEvent; | |
import android.view.ScaleGestureDetector; | |
import android.view.View; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.util.Locale; | |
/** | |
* Created by vinaysshenoy on 16/11/16. | |
*/ | |
public class MatrixTestView extends View implements ScaleGestureDetector.OnScaleGestureListener { | |
public static final String TAG = "MatrixTestView"; | |
private Matrix matrix; | |
private Rect viewPort; | |
private RectF originalRect; | |
private RectF mapRect; | |
private Paint paint; | |
private PointF curTouchPoint; | |
private PointF prevTouchPoint; | |
private PointF scaleFocusPoint; | |
private float minDragThreshold; | |
private float[] matrixScaleHolder; | |
private ScaleGestureDetector scaleGestureDetector; | |
@TouchState | |
private int touchState; | |
public MatrixTestView(Context context) { | |
super(context); | |
init(context, null); | |
} | |
public MatrixTestView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
init(context, attrs); | |
} | |
public MatrixTestView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(context, attrs); | |
} | |
@TargetApi(Build.VERSION_CODES.LOLLIPOP) | |
public MatrixTestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { | |
super(context, attrs, defStyleAttr, defStyleRes); | |
init(context, attrs); | |
} | |
private static void debug(String format, Object... params) { | |
if (params != null) { | |
Log.d(TAG, String.format(Locale.US, format, params)); | |
} else { | |
Log.d(TAG, format); | |
} | |
} | |
/** | |
* Converts a raw pixel value to a dp value, based on the device density | |
*/ | |
private static float pxToDp(float px) { | |
return px / Resources.getSystem().getDisplayMetrics().density; | |
} | |
/** | |
* Converts a raw dp value to a pixel value, based on the device density | |
*/ | |
private static float dpToPx(float dp) { | |
return dp * Resources.getSystem().getDisplayMetrics().density; | |
} | |
private static PointF mapPointsFromTransformedToSourceRect(@NonNull final RectF source, @NonNull final RectF transformed, final float pX, final float pY, @Nullable PointF values) { | |
final PointF point = (values == null) ? new PointF() : values; | |
point.x = source.left + (source.width() * ((pX - transformed.left) / transformed.width())); | |
point.y = source.top + (source.height() * ((pY - transformed.top) / transformed.height())); | |
return point; | |
} | |
private void init(Context context, AttributeSet attributeSet) { | |
viewPort = new Rect(); | |
originalRect = new RectF(); | |
mapRect = new RectF(); | |
curTouchPoint = new PointF(); | |
prevTouchPoint = new PointF(); | |
scaleFocusPoint = new PointF(); | |
matrix = new Matrix(); | |
matrixScaleHolder = new float[2]; | |
minDragThreshold = dpToPx(4F); | |
paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
paint.setColor(Color.BLACK); | |
paint.setStyle(Paint.Style.FILL_AND_STROKE); | |
paint.setStrokeWidth(dpToPx(1.0F)); | |
scaleGestureDetector = new ScaleGestureDetector(context, this); | |
setTouchState(TouchState.NO_TOUCH); | |
} | |
@Override | |
protected void onDraw(Canvas canvas) { | |
super.onDraw(canvas); | |
canvas.drawColor(Color.WHITE); | |
canvas.concat(matrix); | |
canvas.drawRect(originalRect, paint); | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent event) { | |
scaleGestureDetector.onTouchEvent(event); | |
final int numPointers = event.getPointerCount(); | |
boolean invalidate = false; | |
if (numPointers == 1) { | |
invalidate = handleSingleTouch(event); | |
} | |
if (invalidate) { | |
invalidate(); | |
} | |
return true; | |
} | |
private void setTouchState(@TouchState final int state) { | |
touchState = state; | |
String stateLabel; | |
switch (state) { | |
case TouchState.NO_TOUCH: { | |
stateLabel = "NO_TOUCH"; | |
break; | |
} | |
case TouchState.TOUCH_RECT: { | |
stateLabel = "TOUCH_RECT"; | |
break; | |
} | |
case TouchState.TOUCH_VIEW: { | |
stateLabel = "TOUCH_VIEW"; | |
break; | |
} | |
case TouchState.SCALE_RECT: { | |
stateLabel = "SCALE_RECT"; | |
break; | |
} | |
default: { | |
stateLabel = "UNKNOWN"; | |
break; | |
} | |
} | |
debug("Move to touch state: %s", stateLabel); | |
} | |
private boolean handleSingleTouch(final MotionEvent event) { | |
boolean invalidate = false; | |
curTouchPoint.set(event.getX(), event.getY()); | |
matrix.mapRect(mapRect, originalRect); | |
switch (event.getActionMasked()) { | |
case MotionEvent.ACTION_DOWN: { | |
if (mapRect.contains(curTouchPoint.x, curTouchPoint.y)) { | |
setTouchState(TouchState.TOUCH_RECT); | |
prevTouchPoint.set(curTouchPoint); | |
} else { | |
setTouchState(TouchState.TOUCH_VIEW); | |
prevTouchPoint.set(curTouchPoint); | |
} | |
break; | |
} | |
case MotionEvent.ACTION_UP: { | |
setTouchState(TouchState.NO_TOUCH); | |
prevTouchPoint.set(curTouchPoint); | |
break; | |
} | |
case MotionEvent.ACTION_CANCEL: { | |
setTouchState(TouchState.NO_TOUCH); | |
prevTouchPoint.set(curTouchPoint); | |
break; | |
} | |
case MotionEvent.ACTION_MOVE: { | |
if (TouchState.TOUCH_RECT == touchState) { | |
final float diffX = curTouchPoint.x - prevTouchPoint.x; | |
final float diffY = curTouchPoint.y - prevTouchPoint.y; | |
if (Math.abs(diffX) > minDragThreshold || Math.abs(diffY) > minDragThreshold) { | |
matrixScaleHolder = MatrixUtils.scale(matrix, matrixScaleHolder); | |
matrix.preTranslate(diffX / matrixScaleHolder[0], diffY / matrixScaleHolder[1]); | |
prevTouchPoint.set(curTouchPoint); | |
invalidate = true; | |
} | |
} | |
break; | |
} | |
} | |
return invalidate; | |
} | |
@Override | |
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { | |
super.onLayout(changed, left, top, right, bottom); | |
updateDrawingRect(); | |
} | |
private void updateDrawingRect() { | |
getDrawingRect(viewPort); | |
originalRect.set(viewPort); | |
originalRect.right *= 0.25F; | |
originalRect.bottom *= 0.25F; | |
} | |
@Override | |
public boolean onScale(ScaleGestureDetector scaleGestureDetector) { | |
MatrixUtils.clampedPreScale(matrix, scaleGestureDetector.getScaleFactor(), 0.4F, 2.5F, scaleFocusPoint.x, scaleFocusPoint.y); | |
invalidate(); | |
return true; | |
} | |
@Override | |
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { | |
matrix.mapRect(mapRect, originalRect); | |
if (mapRect.contains(scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY())) { | |
scaleFocusPoint = mapPointsFromTransformedToSourceRect(originalRect, mapRect, scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY(), scaleFocusPoint); | |
setTouchState(TouchState.SCALE_RECT); | |
return true; | |
} | |
return false; | |
} | |
@Override | |
public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) { | |
setTouchState(TouchState.NO_TOUCH); | |
} | |
@Retention(RetentionPolicy.SOURCE) | |
@IntDef({ | |
TouchState.NO_TOUCH, | |
TouchState.TOUCH_RECT, | |
TouchState.TOUCH_VIEW, | |
TouchState.SCALE_RECT | |
}) | |
private @interface TouchState { | |
int NO_TOUCH = -1; | |
int TOUCH_RECT = 0; | |
int TOUCH_VIEW = 1; | |
int SCALE_RECT = 2; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.vinaysshenoy.matrixviewtest; | |
import android.graphics.Matrix; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import android.support.annotation.Size; | |
/** | |
* Created by vinaysshenoy on 22/11/16. | |
*/ | |
public final class MatrixUtils { | |
private static final String TAG = "MatrixUtils"; | |
private static final float[] VALUES = new float[9]; | |
private static final Matrix TEMP_MATRIX = new Matrix(); | |
private MatrixUtils() { | |
} | |
private static void throwError(@NonNull String message) { | |
throw new IllegalArgumentException(message); | |
} | |
public static float[] perspective(@NonNull final Matrix matrix, @Nullable @Size(min = 3) final float[] perspective) { | |
if (perspective != null && perspective.length < 3) { | |
throwError("Perspective array should be at least 3 elements in size"); | |
} | |
final float[] perspectiveValues = (perspective != null) ? perspective : new float[3]; | |
matrix.getValues(VALUES); | |
perspectiveValues[0] = VALUES[Matrix.MPERSP_0]; | |
perspectiveValues[1] = VALUES[Matrix.MPERSP_1]; | |
perspectiveValues[2] = VALUES[Matrix.MPERSP_2]; | |
return perspectiveValues; | |
} | |
public static float[] translation(@NonNull final Matrix matrix, @Nullable @Size(min = 2) final float[] translation) { | |
if (translation != null && translation.length < 2) { | |
throwError("Translation array should be at least 2 elements in size"); | |
} | |
final float[] translationValues = (translation != null) ? translation : new float[2]; | |
matrix.getValues(VALUES); | |
translationValues[0] = VALUES[Matrix.MTRANS_X]; | |
translationValues[1] = VALUES[Matrix.MTRANS_Y]; | |
return translationValues; | |
} | |
public static float[] skew(@NonNull final Matrix matrix, @Nullable @Size(min = 2) final float[] skew) { | |
if (skew != null && skew.length < 2) { | |
throwError("Skew array should be at least 2 elements in size"); | |
} | |
final float[] skewValues = (skew != null) ? skew : new float[2]; | |
matrix.getValues(VALUES); | |
skewValues[0] = VALUES[Matrix.MSKEW_X]; | |
skewValues[1] = VALUES[Matrix.MSKEW_Y]; | |
return skewValues; | |
} | |
public static float[] scale(@NonNull final Matrix matrix, @Nullable @Size(min = 2) final float[] scale) { | |
if (scale != null && scale.length < 2) { | |
throwError("Scale array should be at least 2 elements in size"); | |
} | |
final float[] scaleValues = (scale != null) ? scale : new float[2]; | |
matrix.getValues(VALUES); | |
scaleValues[0] = VALUES[Matrix.MSCALE_X]; | |
scaleValues[1] = VALUES[Matrix.MSCALE_Y]; | |
return scaleValues; | |
} | |
public static void clampedPreScale(@NonNull final Matrix matrix, final float scale, final float min, final float max, final float pivotX, final float pivotY) { | |
matrix.getValues(VALUES); | |
final float prevScale = VALUES[Matrix.MSCALE_X]; | |
TEMP_MATRIX.set(matrix); | |
TEMP_MATRIX.preScale(scale, scale, pivotX, pivotY); | |
TEMP_MATRIX.getValues(VALUES); | |
boolean noScale = false; | |
final float matrixScale = VALUES[Matrix.MSCALE_X]; | |
float newScale = scale; | |
if (matrixScale > max) { | |
newScale = 1F + max - prevScale; | |
} else if (matrixScale < min) { | |
newScale = 1F + prevScale - min; | |
} else if (Math.abs(matrixScale - max) <= 0.1F || Math.abs(matrixScale - min) <= 0.1F) { | |
noScale = true; | |
} | |
if (noScale) { | |
return; | |
} | |
matrix.preScale(newScale, newScale, pivotX, pivotY); | |
} | |
public static void clampedPostScale(@NonNull final Matrix matrix, final float scale, final float min, final float max, final float pivotX, final float pivotY) { | |
matrix.getValues(VALUES); | |
final float prevScale = VALUES[Matrix.MSCALE_X]; | |
TEMP_MATRIX.set(matrix); | |
TEMP_MATRIX.postScale(scale, scale, pivotX, pivotY); | |
TEMP_MATRIX.getValues(VALUES); | |
boolean noScale = false; | |
final float matrixScale = VALUES[Matrix.MSCALE_X]; | |
float newScale = scale; | |
if (matrixScale > max) { | |
newScale = 1F + max - prevScale; | |
} else if (matrixScale < min) { | |
newScale = 1F + prevScale - min; | |
} else if (Math.abs(matrixScale - max) <= 0.1F || Math.abs(matrixScale - min) <= 0.1F) { | |
noScale = true; | |
} | |
if (noScale) { | |
return; | |
} | |
matrix.postScale(newScale, newScale, pivotX, pivotY); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment