Prevent CropView from getting confused by multiple pointers.

Not really implementing full multitouch, but ensuring that letting up on
the first finger doesn't just continue the event with another one.

Also refactor some of the MagnifierView's event listening to decouple it
from CropView a bit more.

Bug: 183240525
Test: Touch one edge with one finger, then put another one down and lift
      the first, ensure that movement doesn't continue with second finger.
Change-Id: Ia9f0293a914fb85f80a746f1093a3b23b23eab73
This commit is contained in:
Matt Casey
2021-04-09 13:54:29 -04:00
parent 8ccd7eb538
commit 90ee05903d
2 changed files with 105 additions and 62 deletions

View File

@@ -71,6 +71,7 @@ public class CropView extends View {
private int mImageWidth;
private CropBoundary mCurrentDraggingBoundary = CropBoundary.NONE;
private int mActivePointerId;
// The starting value of mCurrentDraggingBoundary's crop, used to compute touch deltas.
private float mMovementStartValue;
private float mStartingY; // y coordinate of ACTION_DOWN
@@ -138,35 +139,60 @@ public class CropView extends View {
public boolean onTouchEvent(MotionEvent event) {
int topPx = fractionToVerticalPixels(mCrop.top);
int bottomPx = fractionToVerticalPixels(mCrop.bottom);
switch (event.getAction()) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx,
fractionToHorizontalPixels(mCrop.left),
fractionToHorizontalPixels(mCrop.right));
if (mCurrentDraggingBoundary != CropBoundary.NONE) {
mActivePointerId = event.getPointerId(0);
mStartingY = event.getY();
mStartingX = event.getX();
mMovementStartValue = getBoundaryPosition(mCurrentDraggingBoundary);
updateListener(event);
updateListener(MotionEvent.ACTION_DOWN, event.getX());
mMotionRange = getAllowedValues(mCurrentDraggingBoundary);
}
return true;
case MotionEvent.ACTION_MOVE:
if (mCurrentDraggingBoundary != CropBoundary.NONE) {
float deltaPx = isVertical(mCurrentDraggingBoundary) ? event.getY() - mStartingY
: event.getX() - mStartingX;
float delta = pixelDistanceToFraction((int) deltaPx, mCurrentDraggingBoundary);
setBoundaryPosition(mCurrentDraggingBoundary,
mMotionRange.clamp(mMovementStartValue + delta));
updateListener(event);
invalidate();
int pointerIndex = event.findPointerIndex(mActivePointerId);
if (pointerIndex >= 0) {
// Original pointer still active, do the move.
float deltaPx = isVertical(mCurrentDraggingBoundary)
? event.getY(pointerIndex) - mStartingY
: event.getX(pointerIndex) - mStartingX;
float delta = pixelDistanceToFraction((int) deltaPx,
mCurrentDraggingBoundary);
setBoundaryPosition(mCurrentDraggingBoundary,
mMotionRange.clamp(mMovementStartValue + delta));
updateListener(MotionEvent.ACTION_MOVE, event.getX(pointerIndex));
invalidate();
}
return true;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
if (mActivePointerId == event.getPointerId(event.getActionIndex())
&& mCurrentDraggingBoundary != CropBoundary.NONE) {
updateListener(MotionEvent.ACTION_DOWN, event.getX(event.getActionIndex()));
return true;
}
break;
case MotionEvent.ACTION_POINTER_UP:
if (mActivePointerId == event.getPointerId(event.getActionIndex())
&& mCurrentDraggingBoundary != CropBoundary.NONE) {
updateListener(MotionEvent.ACTION_UP, event.getX(event.getActionIndex()));
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (mCurrentDraggingBoundary != CropBoundary.NONE) {
updateListener(event);
if (mCurrentDraggingBoundary != CropBoundary.NONE
&& mActivePointerId == event.getPointerId(mActivePointerId)) {
updateListener(MotionEvent.ACTION_UP, event.getX(0));
return true;
}
break;
}
return super.onTouchEvent(event);
}
@@ -308,12 +334,29 @@ public class CropView extends View {
return null;
}
private void updateListener(MotionEvent event) {
if (mCropInteractionListener != null && (isVertical(mCurrentDraggingBoundary))) {
/**
* @param action either ACTION_DOWN, ACTION_UP or ACTION_MOVE.
* @param x coordinate of the relevant pointer.
*/
private void updateListener(int action, float x) {
if (mCropInteractionListener != null && isVertical(mCurrentDraggingBoundary)) {
float boundaryPosition = getBoundaryPosition(mCurrentDraggingBoundary);
mCropInteractionListener.onCropMotionEvent(event, mCurrentDraggingBoundary,
boundaryPosition, fractionToVerticalPixels(boundaryPosition),
(mCrop.left + mCrop.right) / 2);
switch (action) {
case MotionEvent.ACTION_DOWN:
mCropInteractionListener.onCropDragStarted(mCurrentDraggingBoundary,
boundaryPosition, fractionToVerticalPixels(boundaryPosition),
(mCrop.left + mCrop.right) / 2, x);
break;
case MotionEvent.ACTION_MOVE:
mCropInteractionListener.onCropDragMoved(mCurrentDraggingBoundary,
boundaryPosition, fractionToVerticalPixels(boundaryPosition),
(mCrop.left + mCrop.right) / 2, x);
break;
case MotionEvent.ACTION_UP:
mCropInteractionListener.onCropDragComplete();
break;
}
}
}
@@ -545,12 +588,11 @@ public class CropView extends View {
* Listen for crop motion events and state.
*/
public interface CropInteractionListener {
/**
* Called whenever CropView has a MotionEvent that can impact the position of the crop
* boundaries.
*/
void onCropMotionEvent(MotionEvent event, CropBoundary boundary, float boundaryPosition,
int boundaryPositionPx, float horizontalCenter);
void onCropDragStarted(CropBoundary boundary, float boundaryPosition,
int boundaryPositionPx, float horizontalCenter, float x);
void onCropDragMoved(CropBoundary boundary, float boundaryPosition,
int boundaryPositionPx, float horizontalCenter, float x);
void onCropDragComplete();
}
static class SavedState extends BaseSavedState {

View File

@@ -28,7 +28,6 @@ import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewPropertyAnimator;
@@ -148,49 +147,51 @@ public class MagnifierView extends View implements CropView.CropInteractionListe
}
@Override
public void onCropMotionEvent(MotionEvent event, CropView.CropBoundary boundary,
float cropPosition, int cropPositionPx, float horizontalCenter) {
public void onCropDragStarted(CropView.CropBoundary boundary, float boundaryPosition,
int boundaryPositionPx, float horizontalCenter, float x) {
mCropBoundary = boundary;
mLastCenter = horizontalCenter;
boolean touchOnRight = event.getX() > getParentWidth() / 2;
boolean touchOnRight = x > getParentWidth() / 2;
float translateXTarget = touchOnRight ? 0 : getParentWidth() - getWidth();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastCropPosition = cropPosition;
setTranslationY(cropPositionPx - getHeight() / 2);
setPivotX(getWidth() / 2);
setPivotY(getHeight() / 2);
setScaleX(0.2f);
setScaleY(0.2f);
setAlpha(0f);
setTranslationX((getParentWidth() - getWidth()) / 2);
setVisibility(View.VISIBLE);
mTranslationAnimator =
animate().alpha(1f).translationX(translateXTarget).scaleX(1f).scaleY(1f);
mTranslationAnimator.setListener(mTranslationAnimatorListener);
mTranslationAnimator.start();
break;
case MotionEvent.ACTION_MOVE:
// The touch is near the middle if it's within 10% of the center point.
// We don't want to animate horizontally if the touch is near the middle.
boolean nearMiddle = Math.abs(event.getX() - getParentWidth() / 2)
< getParentWidth() / 10f;
boolean viewOnLeft = getTranslationX() < (getParentWidth() - getWidth()) / 2;
if (!nearMiddle && viewOnLeft != touchOnRight && mTranslationAnimator == null) {
mTranslationAnimator = animate().translationX(translateXTarget);
mTranslationAnimator.setListener(mTranslationAnimatorListener);
mTranslationAnimator.start();
}
mLastCropPosition = cropPosition;
setTranslationY(cropPositionPx - getHeight() / 2);
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
animate().alpha(0).translationX((getParentWidth() - getWidth()) / 2).scaleX(0.2f)
.scaleY(0.2f).withEndAction(() -> setVisibility(View.INVISIBLE)).start();
break;
mLastCropPosition = boundaryPosition;
setTranslationY(boundaryPositionPx - getHeight() / 2);
setPivotX(getWidth() / 2);
setPivotY(getHeight() / 2);
setScaleX(0.2f);
setScaleY(0.2f);
setAlpha(0f);
setTranslationX((getParentWidth() - getWidth()) / 2);
setVisibility(View.VISIBLE);
mTranslationAnimator =
animate().alpha(1f).translationX(translateXTarget).scaleX(1f).scaleY(1f);
mTranslationAnimator.setListener(mTranslationAnimatorListener);
mTranslationAnimator.start();
}
@Override
public void onCropDragMoved(CropView.CropBoundary boundary, float boundaryPosition,
int boundaryPositionPx, float horizontalCenter, float x) {
boolean touchOnRight = x > getParentWidth() / 2;
float translateXTarget = touchOnRight ? 0 : getParentWidth() - getWidth();
// The touch is near the middle if it's within 10% of the center point.
// We don't want to animate horizontally if the touch is near the middle.
boolean nearMiddle = Math.abs(x - getParentWidth() / 2)
< getParentWidth() / 10f;
boolean viewOnLeft = getTranslationX() < (getParentWidth() - getWidth()) / 2;
if (!nearMiddle && viewOnLeft != touchOnRight && mTranslationAnimator == null) {
mTranslationAnimator = animate().translationX(translateXTarget);
mTranslationAnimator.setListener(mTranslationAnimatorListener);
mTranslationAnimator.start();
}
mLastCropPosition = boundaryPosition;
setTranslationY(boundaryPositionPx - getHeight() / 2);
invalidate();
}
@Override
public void onCropDragComplete() {
animate().alpha(0).translationX((getParentWidth() - getWidth()) / 2).scaleX(0.2f)
.scaleY(0.2f).withEndAction(() -> setVisibility(View.INVISIBLE)).start();
}
private Path generateCheckerboard() {