Launcher添加左划负一屏

源码环境是android-API-28,计划在页面处于主页时左划进入负一屏,右滑退出负一屏。从三个方面推进,TouchEvent,Animation 和负一屏View。从Touch的ACTION_DOWN时拦截并处理touch事件,在ACTION_MOVE时拖拽View,在ACTION_UP/ACTION_CANCEL时根据拖拽距离完成剩余动画:返回主页或者前进到负一屏。

  • Touch event

    1. 通过源码研究发现touch事件的分发与传递是从第二层布局(DragLayer)的onInterceptTouchEvent(MotionEvent)开始的,

      @Override 
      public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();  ......
        return findActiveController(ev); 
      }
      
      protected boolean findActiveController(MotionEvent ev) {
        mActiveController = null;......  
        for (TouchController controller : mControllers) {
          if (controller.onControllerInterceptTouchEvent(ev)) {
            mActiveController = controller;
            return true;
          }
        }
        return false;
       }
      
    2. 参照原有launcher3的touch事件处理思路,我们自定义一个TouchController去鉴别处理水平方向的手势,然后注册到UIFactory中。

      public class QuickSearchController extends AbstractStateChangeTouchController{
        QuickSearchController(Launcher l) {
          super(l, SwipeDetector.HORIZONTAL);   
          //SwipeDetector.HORIZONTAL  筛选出水平方向的手势,作用原理在下方会说明
        }......
      public class UiFactory {
        public static TouchController[] createTouchControllers(Launcher launcher) {
          boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher)
              .isSwipeUpGestureEnabled();
          if (!swipeUpEnabled) {
              return new TouchController[] {
                  launcher.getDragController(),
                  new QuickSearchController(launcher),  
                  new OverviewToAllAppsTouchController(launcher),
                  new LauncherTaskViewController(launcher)};
          }
      ......
      
    3. 研究AbstractStateChangeTouchControlleronControllerInterceptTouchEvent

      @Override
      public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          mNoIntercept = !canInterceptTouch(ev);
          if (mNoIntercept) {
              return false;
          }
      
          // Now figure out which direction scroll events the controller will start
          // calling the callbacks.
          final int directionsToDetectScroll;
          boolean ignoreSlopWhenSettling = false;
      
          if (mCurrentAnimation != null) {
              directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
              ignoreSlopWhenSettling = true;
          } else {
              directionsToDetectScroll = getSwipeDirection();
              if (directionsToDetectScroll == 0) {
                  mNoIntercept = true;
                  return false;
              }
          }
          mDetector.setDetectableScrollConditions(directionsToDetectScroll, ignoreSlopWhenSettling);
        }
      
        if (mNoIntercept) {
            return false;
        }
      
        onControllerTouchEvent(ev);
        return mDetector.isDraggingOrSettling();
      }
      

      可得出如下结论:

      1.自定义的子类需要关注canInterceptTouch()的实现,并在正确的场景下返回true以拦截事件使其不会往后往下分发。
      2.如果想拦截Touch事件,getSwipeDirection()不能返回0。
      3.如果actionDown时canInterceptTouch()没有返回true,后续move与up更不会处理

    4. 那么在QuickSearchController实现如下两个方法

        @Override
        protected boolean canInterceptTouch(MotionEvent ev) {
        if (mCurrentAnimation != null ) {
            // If we are already animating from a previous state, we can intercept.
            return true;
        }下面的判断是为了防止频繁快速滑动时currentScreen计算错误导致bug
        if(mLauncher.getWorkspace().isPageInTransition()){
            return false;
        }当处于普通状态的第一页或者搜索页时,请求拦截此事件
        return  (mLauncher.getCurrentWorkspaceScreen() ==0 && mLauncher.isInState(NORMAL))
                || mLauncher.isInState(STATE_SEARCH);
        }
        //以下方法由getSwipeDirection()调用,只要返回值和fromState不同即代表想拦截
        @Override
        protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
            touch点的坐标先小后大是 Negative,先大后小是Positive,即isDragTowardPositive代表右去,反之代表左去
            if(isDragTowardPositive && fromState == STATE_SEARCH){
                return NORMAL;
            }else if(!isDragTowardPositive && fromState == NORMAL && mLauncher.getCurrentWorkspaceScreen() == 0){
                return STATE_SEARCH;
            }
            return fromState;
        }
      
    5. 返回值是mDetector.isDraggingOrSettling(),因此在ACTION_DOWN时,最终return了false,而在ACTIONMOVE时,我们需要mDetector把state修改为dragging从而拦截此事件使其不再向子view分发。参考代码 SwipeDetector.java:

      public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
          case MotionEvent.ACTION_MOVE:
                int pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex == INVALID_POINTER_ID) {
                    break;
                }
                mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos);
                computeVelocity(mDir.getDisplacement(ev, pointerIndex, mLastPos),
                        ev.getEventTime());
      
                // handle state and listener calls.
                if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
                    setState(ScrollState.DRAGGING);
                }
                if (mState == ScrollState.DRAGGING) {
                    reportDragging();
                }
                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
                break;
        ......
      }
      
      public static final int DIRECTION_POSITIVE = 1 << 0;
      public static final int DIRECTION_NEGATIVE = 1 << 1;
      private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
          如果move距离小于8dp,忽略
        if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
                > Math.abs(mDisplacement)) {
            return false;
        }
      
        关注mScrollConditions的位运算。对于一次水平滑动来说:
        如果是从左到右的滑动,那么mScrollConditions必须包含DIRECTION_NEGATIVE才能开始drag
        如果是从右到左的滑动,那么mScrollConditions必须包含DIRECTION_POSITIVE才能开始drag
      
        if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDisplacement > 0) ||
                ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDisplacement < 0)) {
            return true;
        }
        return false;
      }
      

      可以看到要想改变成dragging状态,shouldScrollStart()方法必须返回true才行,因此mScrollConditions的定义值就是关键钥匙。

    6. mScrollConditions的赋值:

      1. SwipeDetector#setDetectableScrollConditions(...)方法定义了这个值,而这个方法在AbstractStateChangeTouchController#onControllerInterceptTouchEvent()的ACTION_DOWN时就调用了。
      2. 根据mScrollConditions = getSwipeDirection()方法,实现getTargetState(),参阅第4步,在想要生效的状态时返回不同于原始参数的返回值即可。
        class AbstractStateChangeTouchController{...
        private int getSwipeDirection() {              
           LauncherState fromState = mLauncher.getStateManager().getState();
           int swipeDirection = 0;
           if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) {
             swipeDirection |= SwipeDetector.DIRECTION_POSITIVE;
           }
           if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) {
             swipeDirection |= SwipeDetector.DIRECTION_NEGATIVE;
           }
           return swipeDirection;
         }
        
    7. 手势抬起事件(ACTION_UP) :同ACTION_CANCEL,如果是dragging状态,则修改为SETTLING状态,最终调用AbstractStateChangeTouchController#onDragEnd(),根据当前拖动位置完成剩余动画,是回滚到主页,还是前进到负一屏。

    8. 至此touchEvent分发部分结束。接下来是animation。

  • Animation while dragging and drag-end

    1. 需要说明的是,传统意义上理解的动画其实只在onDragEnd时触发。在手指拖动屏幕时, 其实是一直在调用setProgress(float)方法直接做translationX位移而已。

    2. 假设最大滑动距离W是屏幕的宽度,即从0往左滑动W像素才能完全显示负一屏,(0-100%),那么每一像素的百分比就是1/W,这个值定义在initCurrentAnimation()方法内, 作为返回值给出。因此当手指拖动屏幕横向滑动距离X时,负一屏需要移动的百分比就是X/W。

    3. 在AbstractStateChangeTouchController#onDrag(...)方法中调用updateProgress(fraction),然后AnimatorPlaybackControllerVL#setPlayFraction(fraction),通过传入的float值计算出当前应当处于的动画位置,然后更新动画进度。这里我仿照AllAppsTransitionController定义一个水平方向的动画控制器,修改动画的targetView和setProgress(progress)方法

       class QuickSearchAnimController implements StateHandler {     copy from AllAppsTransitionController
           private float mShiftRange = Util.SCREEN_WIDTH;      // changes depending on the orientation
      
           public void setProgress(float progress) {
               float shiftCurrent = progress * mShiftRange;
      
               左滑负一屏进入时的translateX变化,由-screenWidth到0,退出时由0到-screenWidth
               mMinusOnePageView.setTranslationX(shiftCurrent - mShiftRange);
      
               主页控件的translateX变化,由0到screenWidth,负一屏退出时由screenWidth变到0
               mLauncher.getWorkspace().setTranslationX(shiftCurrent);
               mLauncher.getWorkspace().getPageIndicator().setTranslationX(shiftCurrent);
               mLauncher.getHotseat().setTranslationX(shiftCurrent);
           } 
      
  • View

    简单实现了一下,自定义一个frameLayout,包含一个输入框EditText即可。我这里使用了仿苹果高斯模糊透明度渐变的效果。有几个注意点记录如下
    • FrameLayout如果需要铺满至全屏幕(包含状态栏和导航栏),需要设置launcher:layout_ignoreInsets="true",或者实现接口Insettable,具体原因参考源码LauncherRootView#fitSystemWindows(Rect)
    • 使用的源图来自wallPaper,需要有READ_EXTERNAL_STORAGE权限,且需要注意性能。毕竟在拖拽时频繁生成bitmap并渲染是很容易导致卡顿。可采用原图缩小后作为blur的原图使用以减小内存消耗。
    • 高斯模糊采用原生RenderScript,透明度渐变思路是:先将原图做blur处理,做完后根据拖拽进度,更改画笔的alpha,然后在自定义frameLayout上画此图作为背景。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容