Skip to content

Instantly share code, notes, and snippets.

@vinaysshenoy
Created November 22, 2016 10:08
Show Gist options
  • Save vinaysshenoy/c03be9892676c2e02fc729ff243eaeff to your computer and use it in GitHub Desktop.
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
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;
}
}
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