Android 手势导航核心实现

一、如何找到入口

Android10推出了全新的手势导航功能,原生的Android系统就提供了此功能,根据这个切入点查询相关实现,Android 10和11的源码里面,在SystemUI 模块里面可以找到对应的关键代码类如下

SystemUI\src\com\android\systemui\statusbar\phone\EdgeBackGestureHandler.java
SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarEdgePanel.java

EdgeBackGestureHandler.java

/**
* Utility class to handle edge swipes for back gesture
*/
public class EdgeBackGestureHandler extends CurrentUserTracker implements DisplayListener,
PluginListener<NavigationEdgeBackPlugin>, ProtoTraceable<SystemUiTraceProto> {
}

这个类是整个返回手势的核心管理类,构造方法如下:

public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService,
SysUiState sysUiFlagContainer, PluginManager pluginManager,
Runnable stateChangeCallback) {
super(Dependency.get(BroadcastDispatcher.class));
// 变量初始化
mContext = context;
mDisplayId = context.getDisplayId();
mMainExecutor = context.getMainExecutor();
mOverviewProxyService = overviewProxyService;
mPluginManager = pluginManager;
mStateChangeCallback = stateChangeCallback;
... ...
}

EdgeBackGestureHandler 类的初始化,以及调用过程,放在 SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java 中实现的,具体逻辑如下:

// SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java
public NavigationBarView(Context context, AttributeSet attrs) {
super(context, attrs);
... ...
// 构造方法中实例化EdgeBackGestureHandler对象
mEdgeBackGestureHandler = new EdgeBackGestureHandler(context, mOverviewProxyService,
mSysUiFlagContainer, mPluginManager, this::updateStates);
}
@Override
public void onNavigationModeChanged(int mode) {
... ...
// 系统导航模式发生变化时回调 (全屏手势导航/按键导航)
mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode);
... ...
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
... ...
// 当NavigationBarView 回调onAttachedToWindow() 时,回调onNavBarAttached(),保持add
// 到window 的时机一致
mEdgeBackGestureHandler.onNavBarAttached();
... ...
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
... ...
// 移除
mEdgeBackGestureHandler.onNavBarDetached();
... ...
}

EdgeBackGestureHandler内部关键代码,注册input事件监听器,实例化NavigationBarEdgePanel

// SystemUI\src\com\android\systemui\statusbar\phone\EdgeBackGestureHandler.java
// 定义一个 input 事件 Reciever
class SysUiInputEventReceiver extends InputEventReceiver {
SysUiInputEventReceiver(InputChannel channel, Looper looper) {
super(channel, looper);
}
public void onInputEvent(InputEvent event) {
EdgeBackGestureHandler.this.onInputEvent(event);
finishInputEvent(event, true);
}
}
// 更新返回手势控制器的状态
// 此方法调用时机:
// 1、EdgeBackGestureHandler.onUserSwitched()
// 2、EdgeBackGestureHandler.onNavBarAttached()
// 3、EdgeBackGestureHandler.onNavBarDetached()
// 4、EdgeBackGestureHandler.onNavigationModeChanged()
private void updateIsEnabled() {
boolean isEnabled = mIsAttached && mIsGesturalModeEnabled;
... ...
if (!mIsEnabled) {
... ...
// Register input event receiver
// 通过 InputMonitor 实现全局手势监听
mInputMonitor = InputManager.getInstance().monitorGestureInput(
"edge-swipe", mDisplayId);
mInputEventReceiver = new SysUiInputEventReceiver(
mInputMonitor.getInputChannel(), Looper.getMainLooper());
// Add a nav bar panel window
// 添加 NavigationBarEdgePanel
setEdgeBackPlugin(new NavigationBarEdgePanel(mContext));
... ...
}
}
// NavigationBarEdgePanel extends NavigationEdgeBackPlugin
private void setEdgeBackPlugin(NavigationEdgeBackPlugin edgeBackPlugin) {
if (mEdgeBackPlugin != null) {
mEdgeBackPlugin.onDestroy();
}
mEdgeBackPlugin = edgeBackPlugin;
// 添加回调,NavigationBarEdgePanel 与 当前类通信
mEdgeBackPlugin.setBackCallback(mBackCallback);
// 创建NavigationBarEdgePanel 参数,并未显示
mEdgeBackPlugin.setLayoutParams(createLayoutParams());
... ...
}

InputMonitor 相关介绍

// android.view.InputMonitor
/**
* An {@code InputMonitor} allows privileged applications and components to monitor streams of
* {@link InputEvent}s without having to be the designated recipient for the event.
(InputMonitor允许特权应用程序和组件监视InputEvent,无需成为事件的指定收件者)
*
* For example, focus dispatched events would normally only go to the focused window on the
* targeted display, but an {@code InputMonitor} will also receive a copy of that event if they're
* registered to monitor that type of event on the targeted display.
*
* @hide
*/
public final class InputMonitor implements Parcelable {
private static final String TAG = "InputMonitor";
private static final boolean DEBUG = false;
// 持有一个 InputChannel 对象引用
private final InputChannel mInputChannel;
... ...
}

实例化代码:

mInputMonitor = InputManager.getInstance().monitorGestureInput("edge-swipe", mDisplayId);
mInputEventReceiver = new SysUiInputEventReceiver(mInputMonitor.getInputChannel(),
Looper.getMainLooper());

InputManager.getInstance().monitorGestureInput() ,InputManager 经过 Binder 将 monitorGestureInput() 的调用传递到 InputManagerService

看看InputEventReceiver 的构造函数:

// android.view.InputEventReceiver
/**
* Provides a low-level mechanism for an application to receive input events.
为应用程序提供接收输入事件 (低级机制)
* @hide
*/
public abstract class InputEventReceiver {
... ...
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
if (inputChannel == null) {
throw new IllegalArgumentException("inputChannel must not be null");
} else if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
} else {
this.mInputChannel = inputChannel;
this.mMessageQueue = looper.getQueue();
this.mReceiverPtr = nativeInit(new WeakReference(this), inputChannel, this.mMessageQueue);
this.mCloseGuard.open("dispose");
}
}
}
// com.android.server.input.InputManagerService.java
// 创建一个输入监视器,该监视器将接收用于系统范围手势解释的指针事件
@Override // Binder call
public InputMonitor monitorGestureInput(String inputChannelName, int displayId) {
... ...
final long ident = Binder.clearCallingIdentity();
try {
InputChannel[] inputChannels = InputChannel.openInputChannelPair(inputChannelName);
InputMonitorHost host = new InputMonitorHost(inputChannels[0]);
nativeRegisterInputMonitor(mPtr, inputChannels[0], displayId,
true /*isGestureMonitor*/);
return new InputMonitor(inputChannels[1], host);
} finally {
Binder.restoreCallingIdentity(ident);
}
}

IMS(InputManagerService) 的 JNI 将负责向 InputDispatcher 发出调用,并将其创建的 Client 端 InputChannel 实例转为 Java 实例返回。
InputMonitor 内部封装了一个 InputChannel 引用,要和普通的 Window 所创建的 InputChannel 区分开来。
这个就是留给某些特权 App 监视输入事件的后门,比如SystemUI,或者后续Android 把手势导航功能转移到的 Launcher

至此,要实现全局监听手势的入口已经理顺了。

二、手势导航(返回)流程整理

  1. 在NavigationBarView 构造方法中 初始化 EdgeBackGestureHandler 对象 ,并且在在 NavigationBarViewonNavigationModeChanged()onAttachedToWindow()onDetachedFromWindow() 中调用 EdgeBackGestureHandler 的对应方法,调用之后,最后会走到 EdgeBackGestureHandlerupdateIsEnabled() 方法;
  2. EdgeBackGestureHandler. updateIsEnabled() 方法中实例化 实例化InputMonitor 对象,并且注册 InputEventReceive监听事件,实现input 事件监听,同时 初始化 NavigationBarEdgePanel ,添加到windwow TIPS:此时NavigationBarEdgePanel 还是不显示的状态
  3. 监听InputEventReceiver.onInputEvent() 方法回调,实现输入事件处理逻辑。

三、输入事件核心处理逻辑

InputEventReceiver.onInputEvent() 中,进入到自定义的处理逻辑中:

private void onMotionEvent(MotionEvent ev) {
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// Verify if this is in within the touch region and we aren't in immersive mode, and
// either the bouncer is showing or the notification panel is hidden
// 判断是否是左边滑动
mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset;
mMLResults = 0;
mLogGesture = false;
mInRejectedExclusion = false;
// 看是否开启了此功能,并且判断是否在排除区域
mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed
&& !mGestureBlockingActivityRunning
&& !QuickStepContract.isBackGestureDisabled(mSysUiFlags)
&& isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
// 把 MotionEvent 传递给 NavigationBarEdgePanel 处理
if (mAllowGesture) {
mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
mEdgeBackPlugin.onMotionEvent(ev);
}
... ...
} else if (mAllowGesture || mLogGesture) {
if (!mThresholdCrossed) {
mEndPoint.x = (int) ev.getX();
mEndPoint.y = (int) ev.getY();
// 多点触碰的情况,直接取消当前 input事件
if (action == MotionEvent.ACTION_POINTER_DOWN) {
if (mAllowGesture) {
logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_MULTI_TOUCH);
// We do not support multi touch for back gesture
cancelGesture(ev);
}
mLogGesture = false;
return;
} else if (action == MotionEvent.ACTION_MOVE) {
// 筛选不合格的其他 输入事件
if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
if (mAllowGesture) {
logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_LONG_PRESS);
cancelGesture(ev);
}
mLogGesture = false;
return;
}
float dx = Math.abs(ev.getX() - mDownPoint.x);
float dy = Math.abs(ev.getY() - mDownPoint.y);
if (dy > dx && dy > mTouchSlop) {
if (mAllowGesture) {
logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_VERTICAL_MOVE);
cancelGesture(ev);
}
mLogGesture = false;
return;
} else if (dx > dy && dx > mTouchSlop) {
if (mAllowGesture) {
mThresholdCrossed = true;
// Capture inputs
// 捕获当前 手势,防止干扰界面
mInputMonitor.pilferPointers();
} else {
logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_FAR_FROM_EDGE);
}
}
}
}
if (mAllowGesture) {
// forward touch
mEdgeBackPlugin.onMotionEvent(ev);
}
}
... ...
}
// frameworks/base/core/java/android/view/InputMonitor.java
/**
* Takes all of the current pointer events streams that are currently being sent to this
* monitor and generates appropriate cancellations for the windows that would normally get
* them.
*
* This method should be used with caution as unexpected pilfering can break fundamental user
* interactions.
*/
public void pilferPointers() {
try {
mHost.pilferPointers();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}

接下来 再看下 NavigationBarEdgePanel 里面的主要代码

public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPlugin {
... ...
}

可看出 NavigationBarEdgePanel 就是一个 自定义view,根据 控制器 传递过来的 MotionEvent 实现具体的UI 效果,并回传事件
**看下 它的事件处理逻辑 **

@Override
public void onMotionEvent(MotionEvent event) {
// MotionEvent 预处理逻辑
... ...
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
... ...
setVisibility(VISIBLE);
// 记录dwon初始坐标点信息
... ...
break;
case MotionEvent.ACTION_MOVE:
handleMoveEvent(event);
break;
case MotionEvent.ACTION_UP:
// 手势抬起,回调
if (mTriggerBack) {
triggerBack();
} else {
cancelBack();
}
... ...
break;
case MotionEvent.ACTION_CANCEL:
cancelBack();
... ...
break;
default:
break;
}
}

其中,move状态下的 handleMoveEvent()是主要的处理逻辑:判断 x 轴的 offset 数值是否达到了阈值 mSwipeThreshold,从而 回调 BackCallback 事件 和当前视图的更新

private void handleMoveEvent(@NonNull MotionEvent event) {
float x = event.getX();
float y = event.getY();
... ...
// Apply a haptic on drag slop passed
// 已经超过阈值的话
// 设置达到触发返回事件条件
if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) {
mDragSlopPassed = true;
mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
mVibrationTime = SystemClock.uptimeMillis();
// Let's show the arrow and animate it in!
mDisappearAmount = 0.0f;
setAlpha(1f);
// And animate it go to back by default!
setTriggerBack(true /* triggerBack */, true /* animated */);
}
// Let's make sure we only go to the baseextend and apply rubberbanding afterwards
// 控制绘制和动画的参数赋值
... ...
// By default we just assume the current direction is kept
boolean triggerBack = mTriggerBack;
// First lets see if we had continuous motion in one direction for a while
if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
triggerBack = mTotalTouchDelta > 0;
}
// 计算方向和偏移值
// Then, let's see if our velocity tells us to change direction
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
float yVelocity = mVelocityTracker.getYVelocity();
float velocity = MathUtils.mag(xVelocity, yVelocity);
mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED,
ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity);
if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) {
mAngleOffset *= -1;
}
// 如果纵向偏移值达到了横向偏移两倍 取消返回事件触发
// Last if the direction in Y is bigger than X * 2 we also abort
if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
triggerBack = false;
}
setTriggerBack(triggerBack, true /* animated */);
... ...
}

手势处理结果

// NavigationBarEdgePanel.java
private void triggerBack() {
// 事件回调到 EdgeBackGestureHandler 进行处理,触发返回事件
mBackCallback.triggerBack();
// 产生 click 振动
if (isSlow
|| SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) {
mVibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK);
}
...
// 隐藏动画的执行
Runnable translationEnd = () -> {
mAngleOffset = Math.max(0, mAngleOffset + 8);
updateAngle(true /* animated */);
mTranslationAnimation.setSpring(mTriggerBackSpring);
setDesiredTranslation(mDesiredTranslation - dp(32), true /* animated */);
// 隐藏视图
animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS)
.withEndAction(() -> setVisibility(GONE));
mArrowDisappearAnimation.start();
scheduleFailsafe();
};
...
}
// 取消事件
private void cancelBack() {
mBackCallback.cancelBack();
if (mTranslationAnimation.isRunning()) {
mTranslationAnimation.addEndListener(mSetGoneEndListener);
} else {
setVisibility(GONE);
}
}

再看下EdgeBackGestureHandler中的 事件处理:

private final NavigationEdgeBackPlugin.BackCallback mBackCallback =
new NavigationEdgeBackPlugin.BackCallback() {
@Override
public void triggerBack() {
sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
// mOverviewProxyService.notifyBackAction(true, (int) mDownPoint.x,
// (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
Log.d(TAG, "triggerBack: ");
}
@Override
public void cancelBack() {
Log.d(TAG, "cancelBack: ");
// mOverviewProxyService.notifyBackAction(false, (int) mDownPoint.x,
// (int) mDownPoint.y, false isButton , !mIsOnLeftEdge);
}
};
private void sendEvent(int action, int code) {
long when = SystemClock.uptimeMillis();
final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
InputDevice.SOURCE_KEYBOARD);
... ...
// 用 InputManager 注入返回事件
InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}

到这儿,整个导航返回手势逻辑及实现就完成了;

扩展 - 多点触控

上面在说到InputEventReceiver.onInputEvent() 的输入事件筛选中,过滤掉了多点触控的情况。
对于Android 系统来说,单点touch 和多点 touch 事件 都是归类于 input事件中的 ,一般而言是处理单点输入事件,多点输入事件同样是能够过滤出来的,所以说类似 两指滑行,五指抓取,三至滑动悬停等等事件 ,都能通过筛选出来,实现对应的逻辑,下边简单介绍一下五指抓取的实现

监听事件输入与上述一致,只是对于具体输入事件的 筛选不同

public void onInputEvent(@NonNull MotionEvent ev) {
... ...
// 触控点 数量
int pCount = ev.getPointerCount();
... ...
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "MotionEvent.ACTION_DOWN pCount: " + ev.getPointerCount());
mLastRadius = radius;
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "MotionEvent.ACTION_UP pCount: " + ev.getPointerCount());
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "MotionEvent.ACTION_CANCEL");
hasVerify = false;
break;
case MotionEvent.ACTION_POINTER_DOWN:
Log.d(TAG, "MotionEvent.ACTION_POINTER_DOWN pCount: " + ev.getPointerCount());
mLastRadius = radius;
mStartEvent = MotionEvent.obtain(ev);
break;
case MotionEvent.ACTION_POINTER_UP:
Log.d(TAG, "ACTION_POINTER_UP ev.getActionIndex()=" + ev.getActionIndex() + " pCount:" + ev.getPointerCount());
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "ACTION_MOVE ev.getActionIndex()" + ev.getActionIndex() + " pCount:" + ev.getPointerCount());
//todo 屏幕识别 五指,过滤 是否是对应的手势,符合条件,进入到对应处理逻辑
if (!hasVerify
&& null != mStartEvent
&& mStartEvent.getPointerCount() >= SVGesturesManager.FIVE_POINTER
&& mStartEvent.getPointerCount() == ev.getPointerCount()) {
final float detaRadius = mLastRadius - radius;
Log.d(TAG, "ACTION_MOVE: detaRadius = " + detaRadius);
if (Math.abs(detaRadius) >= MIN_VELOCITY) {
Log.d(TAG, "手指捏合 pointer count= " + mStartEvent.getPointerCount() + " detaRadius =" + detaRadius);
hasVerify = notifyMultiFingerScale(mStartEvent, displayId);
}
}
break;
... ...
}
}

以下四个是常用的触摸事件的标记:

  1. MotionEvent.ACTION_DOWN
  2. MotionEvent.ACTION_UP
  3. MotionEvent.ACTION_MOVE
  4. MotionEvent.ACTION_CANCEL
    这里面有两个个多点触控场景专用的标记位:
  • MotionEvent.ACTION_POINTER_DOWN
  • MotionEvent.ACTION_POINTER_UP

再看一下 多点触摸的情况下,对应的打印日志
image

posted @   阿丟啊  阅读(2244)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
点击右上角即可分享
微信分享提示