一、如何找到入口
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
至此,要实现全局监听手势的入口已经理顺了。
二、手势导航(返回)流程整理
在NavigationBarView
构造方法中 初始化EdgeBackGestureHandler
对象 ,并且在在NavigationBarView
的onNavigationModeChanged()
、onAttachedToWindow()
、onDetachedFromWindow()
中调用EdgeBackGestureHandler
的对应方法,调用之后,最后会走到EdgeBackGestureHandler
的updateIsEnabled
() 方法;EdgeBackGestureHandler. updateIsEnabled()
方法中实例化实例化InputMonitor
对象,并且注册InputEventReceive
监听事件,实现input 事件监听,同时 初始化NavigationBarEdgePanel
,添加到windwow TIPS:此时NavigationBarEdgePanel 还是不显示的状态- 监听
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;
... ...
}
}
以下四个是常用的触摸事件的标记:
MotionEvent.ACTION_DOWN
MotionEvent.ACTION_UP
MotionEvent.ACTION_MOVE
MotionEvent.ACTION_CANCEL
这里面有两个个多点触控场景专用的标记位:
MotionEvent.ACTION_POINTER_DOWN
MotionEvent.ACTION_POINTER_UP
再看一下 多点触摸的情况下,对应的打印日志