下面列出了android.view.MotionEvent#getHistoricalX ( ) 实例代码,或者点击链接到github查看源代码,也可以在右侧发表评论。
private void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
if (DEBUG_MOVE_EVENT) {
printTouchEvent("onMoveEvent:", x, y, eventTime);
}
if (mIsTrackingForActionDisabled) {
return;
}
if (sGestureEnabler.shouldHandleGesture() && me != null) {
// Add historical points to gesture path.
final int pointerIndex = me.findPointerIndex(mPointerId);
final int historicalSize = me.getHistorySize();
for (int h = 0; h < historicalSize; h++) {
final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
final long historicalTime = me.getHistoricalEventTime(h);
onGestureMoveEvent(historicalX, historicalY, historicalTime,
false /* isMajorEvent */, null);
}
}
if (isShowingMoreKeysPanel()) {
final int translatedX = mMoreKeysPanel.translateX(x);
final int translatedY = mMoreKeysPanel.translateY(y);
mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime);
onMoveKey(x, y);
if (mIsInSlidingKeyInput) {
sDrawingProxy.showSlidingKeyInputPreview(this);
}
return;
}
onMoveEventInternal(x, y, eventTime);
}
private void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
if (DEBUG_MOVE_EVENT) {
printTouchEvent("onMoveEvent:", x, y, eventTime);
}
if (mIsTrackingForActionDisabled) {
return;
}
if (sGestureEnabler.shouldHandleGesture() && me != null) {
// Add historical points to gesture path.
final int pointerIndex = me.findPointerIndex(mPointerId);
final int historicalSize = me.getHistorySize();
for (int h = 0; h < historicalSize; h++) {
final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
final long historicalTime = me.getHistoricalEventTime(h);
onGestureMoveEvent(historicalX, historicalY, historicalTime,
false /* isMajorEvent */, null);
}
}
if (isShowingMoreKeysPanel()) {
final int translatedX = mMoreKeysPanel.translateX(x);
final int translatedY = mMoreKeysPanel.translateY(y);
mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime);
onMoveKey(x, y);
if (mIsInSlidingKeyInput) {
sDrawingProxy.showSlidingKeyInputPreview(this);
}
return;
}
onMoveEventInternal(x, y, eventTime);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
sigPoints.add(new LinePathPoint(x, y, LinePathPoint.TYPE_LINE_START));
if (sigPath.isEmpty()) {
callbacks.onSignatureStarted();
}
sigPath.moveTo(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
int hSize = event.getHistorySize();
for (int i = 0; i < hSize; i++) {
int hX = (int) event.getHistoricalX(i);
int hY = (int) event.getHistoricalY(i);
sigPath.lineTo(hX, hY);
sigPoints.add(new LinePathPoint(x, y, LinePathPoint.TYPE_LINE_POINT));
}
sigPath.lineTo(x, y);
sigPoints.add(new LinePathPoint(x, y, LinePathPoint.TYPE_LINE_POINT));
invalidate();
break;
}
return true;
}
public boolean init(View view, MotionEvent event) {
// We must have history available to calc the dx. Normally it's there - if it isn't temporarily,
// we declare the event 'invalid' and expect it in consequent events.
if (event.getHistorySize() == 0) {
return false;
}
// Allow for counter-orientation-direction operations (e.g. item swiping) to run fluently.
final float dy = event.getY(0) - event.getHistoricalY(0, 0);
final float dx = event.getX(0) - event.getHistoricalX(0, 0);
if (Math.abs(dx) > Math.abs(dy)) {
return false;
}
if( dy == 0.f ) { // just click event
return false;
}
// mAbsOffset = view.getTranslationY();
mAbsOffset = getViewOffset(view);
mDeltaOffset = dy;
mDir = mDeltaOffset > 0;
Log.d("wxy-motion", String.format("mAbsOffset %s mDeltaOffset %s", mAbsOffset, mDeltaOffset));
Log.d("wxy-motion","mDir = " + mDir);
return true;
}
public boolean init(View view, MotionEvent event) {
// We must have history available to calc the dx. Normally it's there - if it isn't temporarily,
// we declare the event 'invalid' and expect it in consequent events.
if (event.getHistorySize() == 0) {
return false;
}
// Allow for counter-orientation-direction operations (e.g. item swiping) to run fluently.
final float dx = event.getX(0) - event.getHistoricalX(0, 0);
final float dy = event.getY(0) - event.getHistoricalY(0, 0);
if (Math.abs(dx) < Math.abs(dy)) {
return false;
}
if( dx == 0.f ) { // just click event
return false;
}
// mAbsOffset = view.getTranslationX();
mAbsOffset = getViewOffset(view);
mDeltaOffset = dx;
mDir = mDeltaOffset > 0;
Log.d("wxy-motion", String.format("mAbsOffset %s mDeltaOffset %s", mAbsOffset, mDeltaOffset));
Log.d("wxy-motion","mDir = " + mDir);
return true;
}
@Test
public void onTouchMoveAction_2ndLeftDragInRightEnd_overscrollLeftFurther() throws Exception {
// Arrange
// Bring UUT to a left-overscroll state
MotionEvent event1 = createShortLeftMoveEvent();
when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
HorizontalElasticityBounceEffect uut = getUUT();
uut.onTouch(mView, event1);
reset(mView);
// Create 2nd left-drag event
MotionEvent event2 = createLongLeftMoveEvent();
// Act
final boolean ret = uut.onTouch(mView, event2);
// Assert
final float expectedTransX1 = (event1.getX() - event1.getHistoricalX(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
final float expectedTransX2 = (event2.getX() - event2.getHistoricalX(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
verify(mView).setTranslationX(expectedTransX2);
verify(mView, never()).setTranslationY(anyFloat());
assertTrue(ret);
assertEquals(STATE_DRAG_END_SIDE, uut.getCurrentState());
// State-change listener called only once?
verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_END_SIDE));
verify(mStateListener).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
// Update-listener called exactly twice?
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(expectedTransX1));
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(expectedTransX2));
verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(eventX, eventY);
lastTouchX = eventX;
lastTouchY = eventY;
// There is no end point yet, so don't waste cycles invalidating.
return true;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
// Start tracking the dirty region.
resetDirtyRect(eventX, eventY);
// When the hardware tracks events faster than they are delivered,
// the
// event will contain a history of those skipped points.
int historySize = event.getHistorySize();
for (int i = 0; i < historySize; i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
expandDirtyRect(historicalX, historicalY);
path.lineTo(historicalX, historicalY);
}
// After replaying history, connect the line to the touch point.
path.lineTo(eventX, eventY);
break;
default:
return false;
}
// Include half the stroke width to avoid clipping.
invalidate((int) (dirtyRect.left - HALF_STROKE_WIDTH),
(int) (dirtyRect.top - HALF_STROKE_WIDTH),
(int) (dirtyRect.right + HALF_STROKE_WIDTH),
(int) (dirtyRect.bottom + HALF_STROKE_WIDTH));
lastTouchX = eventX;
lastTouchY = eventY;
return true;
}
private void handleActionMove(MotionEvent event) {
// Handle all recent motion events so we don't skip any cells even when the device
// is busy...
final float radius = mPathWidth;
final int historySize = event.getHistorySize();
mTmpInvalidateRect.setEmpty();
boolean invalidateNow = false;
for (int i = 0; i < historySize + 1; i++) {
final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
Cell hitCell = detectAndAddHit(x, y);
final int patternSize = mPattern.size();
if (hitCell != null && patternSize == 1) {
mPatternInProgress = true;
notifyPatternStarted();
}
// note current x and y for rubber banding of in progress patterns
final float dx = Math.abs(x - mInProgressX);
final float dy = Math.abs(y - mInProgressY);
if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
invalidateNow = true;
}
if (mPatternInProgress && patternSize > 0) {
final ArrayList<Cell> pattern = mPattern;
final Cell lastCell = pattern.get(patternSize - 1);
float lastCellCenterX = getCenterXForColumn(lastCell.column);
float lastCellCenterY = getCenterYForRow(lastCell.row);
// Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
float left = Math.min(lastCellCenterX, x) - radius;
float right = Math.max(lastCellCenterX, x) + radius;
float top = Math.min(lastCellCenterY, y) - radius;
float bottom = Math.max(lastCellCenterY, y) + radius;
// Invalidate between the pattern's new cell and the pattern's previous cell
if (hitCell != null) {
final float width = mSquareWidth * 0.5f;
final float height = mSquareHeight * 0.5f;
final float hitCellCenterX = getCenterXForColumn(hitCell.column);
final float hitCellCenterY = getCenterYForRow(hitCell.row);
left = Math.min(hitCellCenterX - width, left);
right = Math.max(hitCellCenterX + width, right);
top = Math.min(hitCellCenterY - height, top);
bottom = Math.max(hitCellCenterY + height, bottom);
}
// Invalidate between the pattern's last cell and the previous location
mTmpInvalidateRect.union(Math.round(left), Math.round(top),
Math.round(right), Math.round(bottom));
}
}
mInProgressX = event.getX();
mInProgressY = event.getY();
// To save updates, we only invalidate if the user moved beyond a certain amount.
if (invalidateNow) {
mInvalidate.union(mTmpInvalidateRect);
invalidate(mInvalidate);
mInvalidate.set(mTmpInvalidateRect);
}
}
@Test
public void onTouchMoveAction_undragWhenUpOverscrolled_endOverscrolling() {
// Arrange
// In left & right tests we use equal ratios to avoid the effect's under-scroll handling
final float touchDragRatioFwd = 3f;
final float touchDragRatioBck = 3f;
// Bring UUT to a left-overscroll state
when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
VerticalOverScrollBounceEffectDecorator uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
MotionEvent eventMoveUp = createLongUpwardsMoveEvent();
uut.onTouch(mView, eventMoveUp);
reset(mView);
float startTransX = (eventMoveUp.getX() - eventMoveUp.getHistoricalX(0)) / touchDragRatioFwd;
when(mView.getTranslationX()).thenReturn(startTransX);
// Create the (negative) downwards-drag event
MotionEvent eventMoveDown = createLongDownwardsMoveEvent();
// Act
boolean ret = uut.onTouch(mView, eventMoveDown);
// Assert
verify(mView, never()).setTranslationX(anyFloat());
verify(mView).setTranslationY(0);
assertTrue(ret);
assertEquals(STATE_IDLE, uut.getCurrentState());
// State-change listener invoked to say drag-on and drag-off (idle).
verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_END_SIDE));
verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_DRAG_END_SIDE), eq(STATE_IDLE));
verify(mStateListener, times(2)).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
// Update-listener called exactly twice?
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(startTransX));
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(0f));
verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(@NonNull final View v, @NonNull final MotionEvent event) {
final boolean retVal;
if (!mMotionListenerEnabled || mMotions.isEmpty()) {
if (mOnTouchListener != null) {
retVal = mOnTouchListener.onTouch(v, event);
} else {
retVal = false;
}
return retVal;
}
for (Motion motion : mMotions) {
for (EventImitator imitator : motion.imitators) {
imitator.imitate(v, event);
}
}
if (mOnTouchListener != null) {
retVal = mOnTouchListener.onTouch(v, event);
} else {
retVal = true;
}
if (mRequestDisallowTouchEvent) {
// prevents parent from scrolling or otherwise stealing touch events
v.getParent().requestDisallowInterceptTouchEvent(true);
}
if (v.isClickable()) {
if (event.getEventTime() - event.getDownTime()
> ViewConfiguration.getLongPressTimeout()) {
v.setPressed(false);
return true;
}
if (event.getHistorySize() > 0) {
final float deltaX = event.getHistoricalX(event.getHistorySize() - 1) - event.getX();
final float deltaY = event.getHistoricalY(event.getHistorySize() - 1) - event.getY();
// if user has moved too far, it is no longer a click
final boolean removeClickState = Math.pow(deltaX, 2) + Math.pow(deltaY, 2)
> Math.pow(MAX_CLICK_DISTANCE, 2);
v.setPressed(!removeClickState);
return removeClickState;
} else {
return false;
}
}
return retVal;
}
/** Process incoming touch events */
@SuppressWarnings("unused")
public boolean onTouchEvent(MotionEvent event) {
try {
int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount.invoke(event) : 1;
if (DEBUG)
Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " " + mMode + " " + handleSingleTouchEvents + " " + pointerCount);
if (mMode == MODE_NOTHING && !handleSingleTouchEvents && pointerCount == 1)
// Not handling initial single touch events, just pass them on
return false;
if (DEBUG)
Log.i("MultiTouch", "Got here 2");
// Handle history first (we sometimes get history with ACTION_MOVE events)
int action = event.getAction();
int histLen = event.getHistorySize() / pointerCount;
for (int histIdx = 0; histIdx <= histLen; histIdx++) {
// Read from history entries until histIdx == histLen, then read from current event
boolean processingHist = histIdx < histLen;
if (!multiTouchSupported || pointerCount == 1) {
// Use single-pointer methods -- these are needed as a special case (for some weird reason) even if
// multitouch is supported but there's only one touch point down currently -- event.getX(0) etc. throw
// an exception if there's only one point down.
if (DEBUG)
Log.i("MultiTouch", "Got here 3");
xVals[0] = processingHist ? event.getHistoricalX(histIdx) : event.getX();
yVals[0] = processingHist ? event.getHistoricalY(histIdx) : event.getY();
pressureVals[0] = processingHist ? event.getHistoricalPressure(histIdx) : event.getPressure();
} else {
// Read x, y and pressure of each pointer
if (DEBUG)
Log.i("MultiTouch", "Got here 4");
int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS);
if (DEBUG && pointerCount > MAX_TOUCH_POINTS)
Log.i("MultiTouch", "Got more pointers than MAX_TOUCH_POINTS");
for (int ptrIdx = 0; ptrIdx < numPointers; ptrIdx++) {
int ptrId = (Integer) m_getPointerId.invoke(event, ptrIdx);
pointerIds[ptrIdx] = ptrId;
// N.B. if pointerCount == 1, then the following methods throw an array index out of range exception,
// and the code above is therefore required not just for Android 1.5/1.6 but also for when there is
// only one touch point on the screen -- pointlessly inconsistent :(
xVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalX.invoke(event, ptrIdx, histIdx) : m_getX.invoke(event, ptrIdx));
yVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalY.invoke(event, ptrIdx, histIdx) : m_getY.invoke(event, ptrIdx));
pressureVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalPressure.invoke(event, ptrIdx, histIdx) : m_getPressure
.invoke(event, ptrIdx));
}
}
// Decode event
decodeTouchEvent(pointerCount, xVals, yVals, pressureVals, pointerIds, //
/* action = */processingHist ? MotionEvent.ACTION_MOVE : action, //
/* down = */processingHist ? true : action != MotionEvent.ACTION_UP //
&& (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP //
&& action != MotionEvent.ACTION_CANCEL, //
processingHist ? event.getHistoricalEventTime(histIdx) : event.getEventTime());
}
return true;
} catch (Exception e) {
// In case any of the introspection stuff fails (it shouldn't)
Log.e("MultiTouchController", "onTouchEvent() failed", e);
return false;
}
}
/** Process incoming touch events */
public boolean onTouchEvent(MotionEvent event) {
try {
int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount.invoke(event) : 1;
if (DEBUG)
Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " " + mMode + " " + handleSingleTouchEvents + " " + pointerCount);
if (mMode == MODE_NOTHING && !handleSingleTouchEvents && pointerCount == 1)
// Not handling initial single touch events, just pass them on
return false;
if (DEBUG)
Log.i("MultiTouch", "Got here 2");
// Handle history first (we sometimes get history with ACTION_MOVE events)
int action = event.getAction();
int histLen = event.getHistorySize() / pointerCount;
for (int histIdx = 0; histIdx <= histLen; histIdx++) {
// Read from history entries until histIdx == histLen, then read from current event
boolean processingHist = histIdx < histLen;
if (!multiTouchSupported || pointerCount == 1) {
// Use single-pointer methods -- these are needed as a special case (for some weird reason) even if
// multitouch is supported but there's only one touch point down currently -- event.getX(0) etc. throw
// an exception if there's only one point down.
if (DEBUG)
Log.i("MultiTouch", "Got here 3");
xVals[0] = processingHist ? event.getHistoricalX(histIdx) : event.getX();
yVals[0] = processingHist ? event.getHistoricalY(histIdx) : event.getY();
pressureVals[0] = processingHist ? event.getHistoricalPressure(histIdx) : event.getPressure();
} else {
// Read x, y and pressure of each pointer
if (DEBUG)
Log.i("MultiTouch", "Got here 4");
int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS);
if (DEBUG && pointerCount > MAX_TOUCH_POINTS)
Log.i("MultiTouch", "Got more pointers than MAX_TOUCH_POINTS");
for (int ptrIdx = 0; ptrIdx < numPointers; ptrIdx++) {
int ptrId = (Integer) m_getPointerId.invoke(event, ptrIdx);
pointerIds[ptrIdx] = ptrId;
// N.B. if pointerCount == 1, then the following methods throw an array index out of range exception,
// and the code above is therefore required not just for Android 1.5/1.6 but also for when there is
// only one touch point on the screen -- pointlessly inconsistent :(
xVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalX.invoke(event, ptrIdx, histIdx) : m_getX.invoke(event, ptrIdx));
yVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalY.invoke(event, ptrIdx, histIdx) : m_getY.invoke(event, ptrIdx));
pressureVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalPressure.invoke(event, ptrIdx, histIdx) : m_getPressure
.invoke(event, ptrIdx));
}
}
// Decode event
decodeTouchEvent(pointerCount, xVals, yVals, pressureVals, pointerIds, //
/* action = */processingHist ? MotionEvent.ACTION_MOVE : action, //
/* down = */processingHist ? true : action != MotionEvent.ACTION_UP //
&& (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP //
&& action != MotionEvent.ACTION_CANCEL, //
processingHist ? event.getHistoricalEventTime(histIdx) : event.getEventTime());
}
return true;
} catch (Exception e) {
// In case any of the introspection stuff fails (it shouldn't)
Log.e("MultiTouchController", "onTouchEvent() failed", e);
return false;
}
}
private static PointF getHistoricalPoint(MotionEvent event, int p, int historicalIndex) {
return new PointF(event.getHistoricalX(p, historicalIndex),
event.getHistoricalY(p, historicalIndex));
}
@Test
public void onTouchMoveAction_undragWhenRightOverscrolled_endOverscrolling() {
// Arrange
// In left & right tests we use equal ratios to avoid the effect's under-scroll handling
final float touchDragRatioFwd = 3f;
final float touchDragRatioBck = 3f;
// Bring UUT to a right-overscroll state
when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
HorizontalOverScrollBounceEffectDecorator uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
MotionEvent eventMoveRight = createLongRightMoveEvent();
uut.onTouch(mView, eventMoveRight);
reset(mView);
float startTransX = (eventMoveRight.getX() - eventMoveRight.getHistoricalX(0)) / touchDragRatioFwd;
when(mView.getTranslationX()).thenReturn(startTransX);
// Create the left-drag event
MotionEvent eventMoveLeft = createLongLeftMoveEvent();
// Act
boolean ret = uut.onTouch(mView, eventMoveLeft);
// Assert
verify(mView).setTranslationX(0);
verify(mView, never()).setTranslationY(anyFloat());
assertTrue(ret);
assertEquals(STATE_IDLE, uut.getCurrentState());
// State-change listener invoked to say drag-on and drag-off (idle).
verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_START_SIDE));
verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_DRAG_START_SIDE), eq(STATE_IDLE));
verify(mStateListener, times(2)).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
// Update-listener called exactly twice?
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(startTransX));
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(0f));
verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
}
private void handleActionMove(MotionEvent event) {
// Handle all recent motion events so we don't skip any cells even when the device
// is busy...
final float radius = (mSquareWidth * mDiameterFactor * 0.5f);
final int historySize = event.getHistorySize();
mTmpInvalidateRect.setEmpty();
boolean invalidateNow = false;
for (int i = 0; i < historySize + 1; i++) {
final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
Cell hitCell = detectAndAddHit(x, y);
final int patternSize = mPattern.size();
if (hitCell != null && patternSize == 1) {
mPatternInProgress = true;
notifyPatternStarted();
}
// note current x and y for rubber banding of in progress patterns
final float dx = Math.abs(x - mInProgressX);
final float dy = Math.abs(y - mInProgressY);
if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
invalidateNow = true;
}
if (mPatternInProgress && patternSize > 0) {
final ArrayList<Cell> pattern = mPattern;
final Cell lastCell = pattern.get(patternSize - 1);
float lastCellCenterX = getCenterXForColumn(lastCell.column);
float lastCellCenterY = getCenterYForRow(lastCell.row);
// Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
float left = Math.min(lastCellCenterX, x) - radius;
float right = Math.max(lastCellCenterX, x) + radius;
float top = Math.min(lastCellCenterY, y) - radius;
float bottom = Math.max(lastCellCenterY, y) + radius;
// Invalidate between the pattern's new cell and the pattern's previous cell
if (hitCell != null) {
final float width = mSquareWidth * 0.5f;
final float height = mSquareHeight * 0.5f;
final float hitCellCenterX = getCenterXForColumn(hitCell.column);
final float hitCellCenterY = getCenterYForRow(hitCell.row);
left = Math.min(hitCellCenterX - width, left);
right = Math.max(hitCellCenterX + width, right);
top = Math.min(hitCellCenterY - height, top);
bottom = Math.max(hitCellCenterY + height, bottom);
}
// Invalidate between the pattern's last cell and the previous location
mTmpInvalidateRect.union(Math.round(left), Math.round(top),
Math.round(right), Math.round(bottom));
}
}
mInProgressX = event.getX();
mInProgressY = event.getY();
// To save updates, we only invalidate if the user moved beyond a certain amount.
if (invalidateNow) {
mInvalidate.union(mTmpInvalidateRect);
invalidate(mInvalidate);
mInvalidate.set(mTmpInvalidateRect);
}
}
private void handleActionMove(MotionEvent event) {
// Handle all recent motion events so we don't skip any cells even when
// the device
// is busy...
final float radius = mPathWidth;
final int historySize = event.getHistorySize();
mTmpInvalidateRect.setEmpty();
boolean invalidateNow = false;
for (int i = 0; i < historySize + 1; i++) {
final float x = i < historySize ? event.getHistoricalX(i) : event
.getX();
final float y = i < historySize ? event.getHistoricalY(i) : event
.getY();
Cell hitCell = detectAndAddHit(x, y);
final int patternSize = mPattern.size();
if (hitCell != null && patternSize == 1) {
mPatternInProgress = true;
notifyPatternStarted();
}
// note current x and y for rubber banding of in progress patterns
final float dx = Math.abs(x - mInProgressX);
final float dy = Math.abs(y - mInProgressY);
if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
invalidateNow = true;
}
if (mPatternInProgress && patternSize > 0) {
final ArrayList<Cell> pattern = mPattern;
final Cell lastCell = pattern.get(patternSize - 1);
float lastCellCenterX = getCenterXForColumn(lastCell.column);
float lastCellCenterY = getCenterYForRow(lastCell.row);
// Adjust for drawn segment from last cell to (x,y). Radius
// accounts for line width.
float left = Math.min(lastCellCenterX, x) - radius;
float right = Math.max(lastCellCenterX, x) + radius;
float top = Math.min(lastCellCenterY, y) - radius;
float bottom = Math.max(lastCellCenterY, y) + radius;
// Invalidate between the pattern's new cell and the pattern's
// previous cell
if (hitCell != null) {
final float width = mSquareWidth * 0.5f;
final float height = mSquareHeight * 0.5f;
final float hitCellCenterX = getCenterXForColumn(hitCell.column);
final float hitCellCenterY = getCenterYForRow(hitCell.row);
left = Math.min(hitCellCenterX - width, left);
right = Math.max(hitCellCenterX + width, right);
top = Math.min(hitCellCenterY - height, top);
bottom = Math.max(hitCellCenterY + height, bottom);
}
// Invalidate between the pattern's last cell and the previous
// location
mTmpInvalidateRect.union(Math.round(left), Math.round(top),
Math.round(right), Math.round(bottom));
}
}
mInProgressX = event.getX();
mInProgressY = event.getY();
// To save updates, we only invalidate if the user moved beyond a
// certain amount.
if (invalidateNow) {
mInvalidate.union(mTmpInvalidateRect);
invalidate(mInvalidate);
mInvalidate.set(mTmpInvalidateRect);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(eventX, eventY);
lastTouchX = eventX;
lastTouchY = eventY;
// There is no end point yet, so don't waste cycles invalidating.
return true;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
// Start tracking the dirty region.
resetDirtyRect(eventX, eventY);
// When the hardware tracks events faster than they are delivered, the
// event will contain a history of those skipped points.
int historySize = event.getHistorySize();
for (int i = 0; i < historySize; i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
expandDirtyRect(historicalX, historicalY);
path.lineTo(historicalX, historicalY);
}
// After replaying history, connect the line to the touch point.
path.lineTo(eventX, eventY);
break;
default:
Logs.d("Ignored touch event: " + event.toString());
return false;
}
// Include half the stroke width to avoid clipping.
invalidate(
(int) (dirtyRect.left - HALF_STROKE_WIDTH),
(int) (dirtyRect.top - HALF_STROKE_WIDTH),
(int) (dirtyRect.right + HALF_STROKE_WIDTH),
(int) (dirtyRect.bottom + HALF_STROKE_WIDTH));
lastTouchX = eventX;
lastTouchY = eventY;
return true;
}
/**
* When over-scroll has already started (to the right in this case) and suddenly the user changes
* their mind and scrolls a bit in the other direction:
* <br/>We expect the <b>touch to still be intercepted</b> in that case, and the <b>overscroll to
* remain in effect</b>.
*/
@Test
public void onTouchMoveAction_dragLeftWhenRightOverscolled_continueOverscrollingLeft() throws Exception {
// Arrange
// In left & right tests we use equal ratios to avoid the effect's under-scroll handling
final float touchDragRatioFwd = 3f;
final float touchDragRatioBck = 3f;
// Bring UUT to a right-overscroll state
when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
HorizontalElasticityBounceEffect uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
MotionEvent eventMoveRight = createLongRightMoveEvent();
uut.onTouch(mView, eventMoveRight);
reset(mView);
float startTransX = (eventMoveRight.getX() - eventMoveRight.getHistoricalX(0)) / touchDragRatioFwd;
when(mView.getTranslationX()).thenReturn(startTransX);
// Create the left-drag event
MotionEvent eventMoveLeft = createShortLeftMoveEvent();
// Act
boolean ret = uut.onTouch(mView, eventMoveLeft);
// Assert
float expectedTransX = startTransX +
(eventMoveLeft.getX() - eventMoveLeft.getHistoricalX(0)) / touchDragRatioBck;
verify(mView).setTranslationX(expectedTransX);
verify(mView, never()).setTranslationY(anyFloat());
assertTrue(ret);
assertEquals(STATE_DRAG_START_SIDE, uut.getCurrentState());
// State-change listener called only once?
verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_START_SIDE));
verify(mStateListener).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
// Update-listener called exactly twice?
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(startTransX));
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(expectedTransX));
verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
}
/**
* When over-scroll has already started (to the left in this case) and suddenly the user changes
* their mind and scrolls a bit in the other direction:
* <br/>We expect the <b>touch to still be intercepted</b> in that case, and the <b>overscroll to remain in effect</b>.
*/
@Test
public void onTouchMoveAction_dragRightWhenLeftOverscolled_continueOverscrollingRight() {
// Arrange
// In left & right tests we use equal ratios to avoid the effect's under-scroll handling
final float touchDragRatioFwd = 3f;
final float touchDragRatioBck = 3f;
// Bring UUT to a left-overscroll state
when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
HorizontalOverScrollBounceEffectDecorator uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
MotionEvent eventMoveLeft = createLongLeftMoveEvent();
uut.onTouch(mView, eventMoveLeft);
reset(mView);
float startTransX = (eventMoveLeft.getX() - eventMoveLeft.getHistoricalX(0)) / touchDragRatioFwd;
when(mView.getTranslationX()).thenReturn(startTransX);
// Create the right-drag event
MotionEvent eventMoveRight = createShortRightMoveEvent();
// Act
boolean ret = uut.onTouch(mView, eventMoveRight);
// Assert
float expectedTransX = startTransX + (eventMoveRight.getX() - eventMoveRight.getHistoricalX(0)) / touchDragRatioBck;
verify(mView).setTranslationX(expectedTransX);
verify(mView, never()).setTranslationY(anyFloat());
assertTrue(ret);
assertEquals(STATE_DRAG_END_SIDE, uut.getCurrentState());
// State-change listener called only once?
verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_END_SIDE));
verify(mStateListener).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
// Update-listener called exactly twice?
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(startTransX));
verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(expectedTransX));
verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(eventX, eventY);
lastTouchX = eventX;
lastTouchY = eventY;
// There is no end point yet, so don't waste cycles invalidating.
return true;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
// Start tracking the dirty region.
resetDirtyRect(eventX, eventY);
// When the hardware tracks events faster than they are delivered, the
// event will contain a history of those skipped points.
int historySize = event.getHistorySize();
for (int i = 0; i < historySize; i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
expandDirtyRect(historicalX, historicalY);
path.lineTo(historicalX, historicalY);
}
// After replaying history, connect the line to the touch point.
path.lineTo(eventX, eventY);
break;
default:
return false;
}
// Include half the stroke width to avoid clipping.
invalidate(
(int) (dirtyRect.left - HALF_STROKE_WIDTH),
(int) (dirtyRect.top - HALF_STROKE_WIDTH),
(int) (dirtyRect.right + HALF_STROKE_WIDTH),
(int) (dirtyRect.bottom + HALF_STROKE_WIDTH));
lastTouchX = eventX;
lastTouchY = eventY;
return true;
}