Android 手势导航(Launcher3 部分)
Posted 虫师魁拔
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 手势导航(Launcher3 部分)相关的知识,希望对你有一定的参考价值。
手势导航功能的实现主要由 SystemUI + Launcher3 共同处理,由 OverviewProxyService.java 在 Launcher3 中启动一个Services
Launcher3/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
private void initInputMonitor() {
disposeEventHandlers();
if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
return;
}
Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
mDeviceState.getDisplayId());
mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
// 注册处理 view input 事件,在 onInputEvent 中进行处理
mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
mMainChoreographer, this::onInputEvent);
mDeviceState.updateGestureTouchRegions();
}
... ...
private void onInputEvent(InputEvent ev) {
... ...
final int action = event.getAction();
if (action == ACTION_DOWN) {
... ...
// 判断是手势底部向上滑动
if (mDeviceState.isInSwipeUpTouchRegion(event)) {
... ...
GestureState prevGestureState = new GestureState(mGestureState);
GestureState newGestureState = createGestureState(mGestureState);
mConsumer.onConsumerAboutToBeSwitched();
mGestureState = newGestureState;
// 根据当前实际情况创建不同的 InputConsumer
mConsumer = newConsumer(prevGestureState, mGestureState, event);
mUncheckedConsumer = mConsumer;
... ...
} else {
// 其他 MOVE UP CANCEL 事件处理
if (mUncheckedConsumer != InputConsumer.NO_OP) {
// 处理滑动动画效果
mDeviceState.setOrientationTransformIfNeeded(event);
}
}
boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
&& mConsumer != null
&& !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
// 交由具体的 InputConsumer 去继续处理
mUncheckedConsumer.onMotionEvent(event);
// 结束 reset 状态
if (cleanUpConsumer) {
reset();
}
}
TouchInteractionService 是 Launcher 中开始地方
initInputMonitor() 函数中注册 onInputEvent 事件监听。这个 onInputEvent 从 BatchedInputEventReceiver(继承 InputEventReceiver.java) 的 onInputEvent 调用。
onInputEvent 函数中处理滑动事件,在 DOWN 事件时根据不同的场景创建不同的 InputConsumer,例如在桌面、或其他界面等不同情况下使用手势,对应的 InputConsumer 是不同的,最常见的就是 OtherActivityInputConsumer (其他Activity界面使用手势导航)。
Launcher3/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
public void onMotionEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case ACTION_DOWN: {
// 非关键代码
break;
}
case ACTION_MOVE: {
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == INVALID_POINTER_ID) {
break;
}
mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
float displacement = getDisplacement(ev);
float displacementX = mLastPos.x - mDownPos.x;
float displacementY = mLastPos.y - mDownPos.y;
if (!mPassedWindowMoveSlop) {
if (!mIsDeferredDownTarget) {
// Normal gesture, ensure we pass the drag slop before we start tracking
// the gesture
if (Math.abs(displacement) > mTouchSlop) {
mPassedWindowMoveSlop = true;
mStartDisplacement = Math.min(displacement, -mTouchSlop);
}
}
}
float horizontalDist = Math.abs(displacementX);
float upDist = -displacement;
boolean passedSlop = squaredHypot(displacementX, displacementY)
>= mSquaredTouchSlop;
if (!mPassedSlopOnThisGesture && passedSlop) {
mPassedSlopOnThisGesture = true;
}
// Until passing slop, we don't know what direction we're going, so assume
// we're quick switching to avoid translating recents away when continuing
// the gesture (in which case mPassedPilferInputSlop starts as true).
boolean haveNotPassedSlopOnContinuedGesture =
!mPassedSlopOnThisGesture && mPassedPilferInputSlop;
boolean isLikelyToStartNewTask = haveNotPassedSlopOnContinuedGesture
|| horizontalDist > upDist;
if (!mPassedPilferInputSlop) {
if (passedSlop) {
if (mDisableHorizontalSwipe
&& Math.abs(displacementX) > Math.abs(displacementY)) {
// Horizontal gesture is not allowed in this region
forceCancelGesture(ev);
break;
}
mPassedPilferInputSlop = true;
if (mIsDeferredDownTarget) {
// 启动动画
startTouchTrackingForWindowAnimation(ev.getEventTime());
}
if (!mPassedWindowMoveSlop) {
mPassedWindowMoveSlop = true;
mStartDisplacement = Math.min(displacement, -mTouchSlop);
}
// 通知开始手势滑动
notifyGestureStarted(isLikelyToStartNewTask);
}
}
if (mInteractionHandler != null) {
if (mPassedWindowMoveSlop) {
// 更新移动位置
mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
}
// 更新移动检测
if (mDeviceState.isFullyGesturalNavMode()) {
mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
|| isLikelyToStartNewTask);
mMotionPauseDetector.addPosition(ev);
mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
}
}
break;
}
case ACTION_CANCEL:
case ACTION_UP: {
if (DEBUG_FAILED_QUICKSWITCH && !mPassedWindowMoveSlop) {
float displacementX = mLastPos.x - mDownPos.x;
float displacementY = mLastPos.y - mDownPos.y;
Log.d("Quickswitch", "mPassedWindowMoveSlop=false"
+ " disp=" + squaredHypot(displacementX, displacementY)
+ " slop=" + mSquaredTouchSlop);
}
finishTouchTracking(ev);
break;
}
}
}
OtherActivityInputConsumer 是具体处理的类。主要都在 onMotionEvent ACTION_MOVE 事件做处理。
startTouchTrackingForWindowAnimation 函数中进行 mInteractionHandler 等初始化操作及设置动画开始。
notifyGestureStarted 函数中设置开始手势滑动状态。
接下来的 if (mInteractionHandler != null) 代码块中就是具体滑动时候的动画缩放显示等操作。
finishTouchTracking(ev) 函数中通知滑动结束,通知最终状态。
private void finishTouchTracking(MotionEvent ev) {
... ...
if (mPassedWindowMoveSlop && mInteractionHandler != null) {
if (ev.getActionMasked() == ACTION_CANCEL) {
// 手势滑动取消
mInteractionHandler.onGestureCancelled();
} else {
// 手势滑动正常结束
mVelocityTracker.computeCurrentVelocity(1000,
ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
float velocity = mNavBarPosition.isRightEdge()
? velocityX
: mNavBarPosition.isLeftEdge()
? -velocityX
: velocityY;
// up 动作时最后修改一次位置
mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
// 通知滑动结束
mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY),
mDownPos);
}
}
... ...
}
判断最终是执行的 HOMO 还是 RECENTS 等事件是在 mInteractionHandler (BaseSwipeUpHandlerV2.java) 中根据滑动中的数据具体判断。
Launcher3/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
float flingThreshold = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_velocity);
boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
if (isVelocityVertical) {
mLogDirection = velocity.y < 0 ? Direction.UP : Direction.DOWN;
} else {
mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
}
mDownPos = downPos;
handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
}
private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity,
boolean isCancel) {
PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000);
long duration = MAX_SWIPE_DURATION;
float currentShift = mCurrentShift.value;
// 根据滑动数值判断最终是什么类型事件
final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
isFling, isCancel);
float endShift = endTarget.isLauncher ? 1 : 0;
final float startShift;
Interpolator interpolator = DEACCEL;
if (!isFling) {
long expectedDuration = Math.abs(Math.round((endShift - currentShift)
* MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
startShift = currentShift;
interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
} else {
startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y
* getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
float minFlingVelocity = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_min_velocity);
if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
if (endTarget == RECENTS && !mDeviceState.isFullyGesturalNavMode()) {
Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
startShift, endShift, endShift, endVelocity / 1000,
mTransitionDragLength, mContext);
endShift = overshoot.end;
interpolator = overshoot.interpolator;
duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION,
MAX_SWIPE_DURATION);
} else {
float distanceToTravel = (endShift - currentShift) * mTransitionDragLength;
// we want the page's snap velocity to approximately match the velocity at
// which the user flings, so we scale the duration by a value near to the
// derivative of the scroll interpolator at zero, ie. 2.
long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
if (endTarget == RECENTS) {
interpolator = OVERSHOOT_1_2;
}
}
}
}
if (endTarget.isLauncher && mRecentsAnimationController != null) {
mRecentsAnimationController.enableInputProxy(mInputConsumer,
this::createNewInputProxyHandler);
}
if (endTarget == HOME) {
setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
} else if (endTarget == RECENTS) {
LiveTileOverlay.INSTANCE.startIconAnimation();
if (mRecentsView != null) {
int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
if (mRecentsView.getNextPage() != nearestPage) {
// We shouldn't really scroll to the next page when swiping up to recents.
// Only allow settling on the next page if it's nearest to the center.
mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration));
}
if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) {
mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION);
}
duration = Math.max(duration, mRecentsView.getScroller().getDuration());
}
if (mDeviceState.isFullyGesturalNavMode()) {
setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
}
}
// Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
// or resumeLastTask().
if (mRecentsView != null) {
mRecentsView.setOnPageTransitionEndCallback(
() -> mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED));
} else {
mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
}
animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
}
最终以 handleNormalGestureEnd 结束,这里 calculateEndTarget 进行判断最终的手势滑动动作是哪种
系统设置有四种手势动作:
HOME 回到主界面
RECENTS 多任务界面
NEW_TASK 切换到新的应用
LAST_TASK 仍然停留在当前界面
以上是关于Android 手势导航(Launcher3 部分)的主要内容,如果未能解决你的问题,请参考以下文章
Android 13 返回导航大变更:返回键彻底废弃 + 可预见型返回手势
Android 13 返回导航大变更:返回键彻底废弃 + 可预见型返回手势