Moved assistant gesture to corners

The corners will be separated with quick switch by detecting at the slop
of the angle from touch down to that position. If over 30 deg then
assistant will be tracked otherwise quick switch while swipe up will not
be tracked at all.

Test: manual
Bug: 112934365
Change-Id: I6a3aeb1509d9706696a30ef1fba3ce7e3e5ec07c
This commit is contained in:
Matthew Ng 2019-03-15 12:41:41 -07:00
parent da60846197
commit a3b03418c1
4 changed files with 153 additions and 113 deletions

View File

@ -22,59 +22,70 @@ import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import com.android.launcher3.anim.Interpolators;
import com.android.quickstep.util.MotionPauseDetector;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.system.NavigationBarCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
import com.android.launcher3.R;
import com.android.systemui.shared.system.NavigationBarCompat;
/**
* Touch consumer for handling events to launch assistant from launcher
*/
public class AssistantTouchConsumer implements InputConsumer {
private static final String TAG = "AssistantTouchConsumer";
private static final long RETRACT_ANIMATION_DURATION_MS = 300;
/* The assistant touch consume competes with quick switch InputConsumer gesture. The delegate
* can be chosen to run if the angle passing the slop is lower than the threshold angle. When
* this occurs, the state changes to {@link #STATE_DELEGATE_ACTIVE} where the next incoming
* motion events are handled by the delegate instead of the assistant touch consumer. If the
* angle is higher than the threshold, the state will change to {@link #STATE_ASSISTANT_ACTIVE}.
*/
private static final int STATE_INACTIVE = 0;
private static final int STATE_ASSISTANT_ACTIVE = 1;
private static final int STATE_DELEGATE_ACTIVE = 2;
private final PointF mDownPos = new PointF();
private final PointF mLastPos = new PointF();
private final PointF mStartDragPos = new PointF();
private int mActivePointerId = -1;
private final int mDisplayRotation;
private final Rect mStableInsets = new Rect();
private final float mDragSlop;
private final float mTouchSlop;
private final float mThreshold;
private float mStartDisplacement;
private boolean mPassedDragSlop;
private boolean mPassedTouchSlop;
private long mPassedTouchSlopTime;
private boolean mPassedSlop;
private boolean mLaunchedAssistant;
private float mDistance;
private float mTimeFraction;
private long mDragTime;
private float mLastProgress;
private int mState;
private final float mDistThreshold;
private final long mTimeThreshold;
private final int mAngleThreshold;
private final float mSlop;
private final MotionPauseDetector mMotionPauseDetector;
private final ISystemUiProxy mSysUiProxy;
private final InputConsumer mConsumerDelegate;
public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy) {
public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
InputConsumer delegate) {
final Resources res = context.getResources();
mSysUiProxy = systemUiProxy;
mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx();
mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx();
mThreshold = context.getResources().getDimension(R.dimen.gestures_assistant_threshold);
Display display = context.getSystemService(WindowManager.class).getDefaultDisplay();
mDisplayRotation = display.getRotation();
WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
mConsumerDelegate = delegate;
mMotionPauseDetector = new MotionPauseDetector(context);
mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
mSlop = NavigationBarCompat.getQuickScrubTouchSlopPx();
mState = STATE_INACTIVE;
}
@Override
@ -82,15 +93,29 @@ public class AssistantTouchConsumer implements InputConsumer {
return TYPE_ASSISTANT;
}
@Override
public boolean isActive() {
return mState != STATE_INACTIVE;
}
@Override
public void onMotionEvent(MotionEvent ev) {
// TODO add logging
switch (ev.getActionMasked()) {
case ACTION_DOWN: {
mActivePointerId = ev.getPointerId(0);
mDownPos.set(ev.getX(), ev.getY());
mLastPos.set(mDownPos);
mLastProgress = -1;
mTimeFraction = 0;
// Detect when the gesture decelerates to start the assistant
mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
if (isPaused && mState == STATE_ASSISTANT_ACTIVE) {
mTimeFraction = 1;
updateAssistantProgress();
}
});
break;
}
case ACTION_POINTER_UP: {
@ -107,94 +132,100 @@ public class AssistantTouchConsumer implements InputConsumer {
break;
}
case ACTION_MOVE: {
if (mState == STATE_DELEGATE_ACTIVE) {
break;
}
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
break;
}
mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
float displacement = getDisplacement(ev);
if (!mPassedDragSlop) {
// Normal gesture, ensure we pass the drag slop before we start tracking
// the gesture
if (Math.abs(displacement) > mDragSlop) {
mPassedDragSlop = true;
mStartDisplacement = displacement;
mPassedTouchSlopTime = SystemClock.uptimeMillis();
}
}
if (!mPassedSlop) {
// Normal gesture, ensure we pass the slop before we start tracking the gesture
if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) > mSlop) {
mPassedSlop = true;
mStartDragPos.set(mLastPos.x, mLastPos.y);
mDragTime = SystemClock.uptimeMillis();
if (!mPassedTouchSlop) {
if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) >=
mTouchSlop) {
mPassedTouchSlop = true;
if (!mPassedDragSlop) {
mPassedDragSlop = true;
mStartDisplacement = displacement;
mPassedTouchSlopTime = SystemClock.uptimeMillis();
// Determine if angle is larger than threshold for assistant detection
float angle = (float) Math.toDegrees(
Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x));
angle = angle > 90 ? 180 - angle : angle;
if (angle > mAngleThreshold) {
mState = STATE_ASSISTANT_ACTIVE;
if (mConsumerDelegate != null) {
// Send cancel event
MotionEvent event = MotionEvent.obtain(ev);
event.setAction(MotionEvent.ACTION_CANCEL);
mConsumerDelegate.onMotionEvent(event);
}
} else {
mState = STATE_DELEGATE_ACTIVE;
}
}
}
if (mPassedDragSlop) {
// Move
float distance = mStartDisplacement - displacement;
if (distance >= 0) {
onAssistantProgress(distance / mThreshold);
} else {
// Movement
mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
mLastPos.y - mStartDragPos.y);
mMotionPauseDetector.addPosition(mDistance, 0);
if (mDistance >= 0) {
final long diff = SystemClock.uptimeMillis() - mDragTime;
mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
updateAssistantProgress();
}
}
break;
}
case ACTION_CANCEL:
break;
case ACTION_UP: {
if (ev.getEventTime() - mPassedTouchSlopTime < ViewConfiguration.getTapTimeout()) {
onAssistantProgress(1);
case ACTION_UP:
if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) {
ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0)
.setDuration(RETRACT_ANIMATION_DURATION_MS);
animator.addUpdateListener(valueAnimator -> {
float progress = (float) valueAnimator.getAnimatedValue();
try {
mSysUiProxy.onAssistantProgress(progress);
} catch (RemoteException e) {
Log.w(TAG, "Failed to send SysUI start/send assistant progress: "
+ progress, e);
}
});
animator.setInterpolator(Interpolators.DEACCEL_2);
animator.start();
}
mMotionPauseDetector.clear();
break;
}
}
if (mState != STATE_ASSISTANT_ACTIVE && mConsumerDelegate != null) {
mConsumerDelegate.onMotionEvent(ev);
}
}
private void onAssistantProgress(float progress) {
if (mLastProgress == progress) {
return;
}
try {
mSysUiProxy.onAssistantProgress(Math.max(0, Math.min(1, progress)));
if (progress >= 1 && !mLaunchedAssistant) {
mSysUiProxy.startAssistant(new Bundle());
mLaunchedAssistant = true;
}
private void updateAssistantProgress() {
if (!mLaunchedAssistant) {
float progress = Math.min(mDistance * 1f / mDistThreshold, 1) * mTimeFraction;
mLastProgress = progress;
} catch (RemoteException e) {
Log.w(TAG, "Failed to notify SysUI to start/send assistant progress: " + progress, e);
try {
mSysUiProxy.onAssistantProgress(progress);
if (mDistance >= mDistThreshold && mTimeFraction >= 1) {
mSysUiProxy.startAssistant(new Bundle());
mLaunchedAssistant = true;
}
} catch (RemoteException e) {
Log.w(TAG, "Failed to send SysUI start/send assistant progress: " + progress, e);
}
}
}
private boolean isNavBarOnRight() {
return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0;
}
private boolean isNavBarOnLeft() {
return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0;
}
private float getDisplacement(MotionEvent ev) {
float eventX = ev.getX();
float eventY = ev.getY();
float displacement = eventY - mDownPos.y;
if (isNavBarOnRight()) {
displacement = eventX - mDownPos.x;
} else if (isNavBarOnLeft()) {
displacement = mDownPos.x - eventX;
}
return displacement;
}
static boolean withinTouchRegion(Context context, float x) {
return x > context.getResources().getDisplayMetrics().widthPixels
- context.getResources().getDimension(R.dimen.gestures_assistant_width);
static boolean withinTouchRegion(Context context, MotionEvent ev) {
final Resources res = context.getResources();
final int width = res.getDisplayMetrics().widthPixels;
final int height = res.getDisplayMetrics().heightPixels;
final int size = res.getDimensionPixelSize(R.dimen.gestures_assistant_size);
return (ev.getX() > width - size || ev.getX() < size) && ev.getY() > height - size;
}
}

View File

@ -313,31 +313,36 @@ public class TouchInteractionService extends Service {
mSwipeSharedState.clearAllState();
}
final ActivityControlHelper activityControl =
mOverviewComponentObserver.getActivityControlHelper();
if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) {
return InputConsumer.NO_OP;
} else if (mAssistantAvailable && mOverviewInteractionState.isSwipeUpGestureEnabled()
&& FeatureFlags.ENABLE_ASSISTANT_GESTURE.get()
&& AssistantTouchConsumer.withinTouchRegion(this, event.getX())) {
return new AssistantTouchConsumer(this, mRecentsModel.getSystemUiProxy());
} else if (mSwipeSharedState.goingToLauncher ||
mOverviewComponentObserver.getActivityControlHelper().isResumed()) {
return OverviewInputConsumer.newInstance(
mOverviewComponentObserver.getActivityControlHelper(), false);
&& AssistantTouchConsumer.withinTouchRegion(this, event)) {
return new AssistantTouchConsumer(this, mISystemUiProxy, !activityControl.isResumed()
? createOtherActivityInputConsumer(event, runningTaskInfo) : null);
} else if (mSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
return OverviewInputConsumer.newInstance(activityControl, false);
} else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
mOverviewComponentObserver.getActivityControlHelper().isInLiveTileMode()) {
return OverviewInputConsumer.newInstance(
mOverviewComponentObserver.getActivityControlHelper(), false);
activityControl.isInLiveTileMode()) {
return OverviewInputConsumer.newInstance(activityControl, false);
} else {
ActivityControlHelper activityControl =
mOverviewComponentObserver.getActivityControlHelper();
boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event);
return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel,
mOverviewComponentObserver.getOverviewIntent(), activityControl,
shouldDefer, mOverviewCallbacks, mTaskOverlayFactory, mInputConsumer,
this::onConsumerInactive, mSwipeSharedState);
return createOtherActivityInputConsumer(event, runningTaskInfo);
}
}
private OtherActivityInputConsumer createOtherActivityInputConsumer(MotionEvent event,
RunningTaskInfo runningTaskInfo) {
final ActivityControlHelper activityControl =
mOverviewComponentObserver.getActivityControlHelper();
boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event);
return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel,
mOverviewComponentObserver.getOverviewIntent(), activityControl,
shouldDefer, mOverviewCallbacks, mTaskOverlayFactory, mInputConsumer,
this::onConsumerInactive, mSwipeSharedState);
}
/**
* To be called by the consumer when it's no longer active.
*/

View File

@ -26,4 +26,8 @@
determines how many thumbnails will be fetched in the background. -->
<integer name="recentsThumbnailCacheSize">3</integer>
<integer name="recentsIconCacheSize">12</integer>
<!-- Assistant Gesture -->
<integer name="assistant_gesture_min_time_threshold">200</integer>
<integer name="assistant_gesture_corner_deg_threshold">30</integer>
</resources>

View File

@ -66,6 +66,6 @@
<dimen name="shelf_surface_offset">24dp</dimen>
<!-- Assistant Gestures -->
<dimen name="gestures_assistant_width">70dp</dimen>
<dimen name="gestures_assistant_threshold">200dp</dimen>
<dimen name="gestures_assistant_size">28dp</dimen>
<dimen name="gestures_assistant_drag_threshold">70dp</dimen>
</resources>