Android 手势导航(从下往上滑动进入多任务页面)

文章详细介绍了Android系统启动的各个阶段,包括init、zygote、SystemServer和launcher的启动流程。接着深入探讨了手势导航功能的实现,特别是Launcher3中的TouchInteractionService如何监听和处理手势事件,如滑动向上进入多任务页面。文章还涉及了不同InputConsumer在不同场景下的应用,以及手势结束时的处理逻辑,如判定是回到主界面还是多任务界面。整个过程揭示了Android系统中手势识别和系统服务的交互机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        

  Android系统启动篇

1,《android系统启动流程简介》

2,《android init进程启动流程》

3,《android zygote进程启动流程》

4,《Android SystemServer进程启动流程》

5,《android launcher启动流程》

6,《Android Activity启动过程详解》

Android系统开发准备篇

1,《Android 源码下载和编译》

2,《android 11源码编译和pixel3 刷机》

3,《Android Framework代码IDE加载和调试》

Android系统开发实践篇

1,《android设置默认输入法》

2,《android framework预制APK应用》

3,《Android系统层面限制应用开机自启动详解》

4,《android单独编译framework模块并push》

5,《Android Framework开发系统问题分析》

Android系统开发核心知识储备篇

1,《Android编译系统-envsetup和lunch代码篇》

2,《Android编译系统-概念篇》

3,《android日志系统详解》

4,《Android系统Handler详解》

5,《Android系统Binder详解》

6,《Android中Activity、View和Window关系详解》

7,《android view绘制流程详解》

8,《Android读取系统属性详解》

9,《android 窗口管理机制详解》

10,《初识Android系统》

11,《android中AMS进程通知Zygote进程fork新进程的通信方式》

Android核心功能详解篇

1,《android应用市场点击下载APK安装详解》

2,《Android 手势导航(从下往上滑动进入多任务页面)》

3,《android手势分析(应用界面左往右边滑动退出应用)》

4,《android应用安装流程详解》

5,《android11安装应用触发桌面图标刷新流程》

6,《Android系统多任务Recents详解》

7,《android系统导航栏视图分析》

———————————————————————————————————————————

        手势导航功能的实现主要由 SystemUI + Launcher3 共同处理,SystemUI 中主要由 OverviewProxyService.java 监听,而在 Launcher3 中启动一个 TouchInteractionService 服务监听,主要代码实现都由 Launcher 中处理。

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 进行判断最终的手势滑动动作是哪种。

系统设置有四种手势动作:

1,HOME 回到主界面

2,RECENTS 多任务界面

3,NEW_TASK 切换到新的应用

4,LAST_TASK 仍然停留在当前界面

手势动作,从底部往上滑,启动RECENTS 多任务界面

动画效果会让背景页面可见,回到luncher3页面,加载QuickstepLauncher,其中LauncherRecentsView加载应用的容器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

佳哥的技术分享

创作不易,谢谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值