首页 > 其他分享 >Android 手势导航核心实现

Android 手势导航核心实现

时间:2022-12-13 15:11:06浏览次数:37  
标签:MotionEvent ... 导航 void EdgeBackGestureHandler ACTION Android ev 手势

一、如何找到入口

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

标签:MotionEvent,...,导航,void,EdgeBackGestureHandler,ACTION,Android,ev,手势
From: https://www.cnblogs.com/qiyuexiaxun/p/16978868.html

相关文章