Android7.0的代码开始,Launcher3可以在Hotseat的区域通过向上滑动打开应用列表界面。本文我们就来详细分析下这个过程吧。

一开始以为是Hotseat里面有onTouchEvent相关的逻辑处理,但是查看代码发现里面基本没有相关的事件处理逻辑,所以我们只能看下布局文件了,由事件分发的机制我们知道,如果子view不拦截处理事件,则它就会向下传递给父容器处理。Hotseat的父容器就是DragLayer。DragLayer不仅有拖拽事件的处理,还把Hotseat的滑动事件传递给AllAppsTransitionController处理。

packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragLayer.java
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ...

        if (mDragController.onInterceptTouchEvent(ev)) {
            mActiveController = mDragController;
            return true;
        }

        if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && mAllAppsController.onInterceptTouchEvent(ev)) {
            mActiveController = mAllAppsController;
            return true;
        }

        ...
        return false;
    }

DragLayer中先判断此事件是否被拖拽事件消费,如果没有再给AllAppsTransitionController处理。其中通过修改FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP,就可以定制化是否允许向上滑动显示应用列表界面。

packages\apps\Launcher3\src\com\android\launcher3\allapps\AllAppsTransitionController.java
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ...
        if (mNoIntercept) {
            return false;
        }
        mDetector.onTouchEvent(ev);
        if (mDetector.isSettlingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) {
            return false;
        }
        return mDetector.isDraggingOrSettling();
    }

函数一开始是一大段用来计算mNoIntercept变量的,当mNoIntercept为真就不拦截此事件,要滑动显示应用列表就必须拦截事件,所以会调VerticalPullDetector的onTouchEvent()方法。我们来看下VerticalPullDetector的主要代码:

packages\apps\Launcher3\src\com\android\launcher3\allapps\VerticalPullDetector.java
    enum ScrollState {
        IDLE,
        DRAGGING,      // onDragStart, onDrag
        SETTLING       // onDragEnd
    }
    ...    
    interface Listener {
        void onDragStart(boolean start);
        boolean onDrag(float displacement, float velocity);
        void onDragEnd(float velocity, boolean fling);
    }
    ...
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = ev.getX();
                mDownY = ev.getY();
                mLastDisplacement = 0;
                mDisplacementY = 0;
                mVelocity = 0;

                if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
                    setState(ScrollState.DRAGGING);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mDisplacementX = ev.getX() - mDownX;
                mDisplacementY = ev.getY() - mDownY;
                computeVelocity(ev);
                Log.i(TAG,"moving mDisplacementY=" +mDisplacementY+",mDisplacementX=" +mDisplacementX);
                // handle state and listener calls.
                if (mState != ScrollState.DRAGGING && shouldScrollStart()) {
                    setState(ScrollState.DRAGGING);
                }
                if (mState == ScrollState.DRAGGING) {
                    reportDragging();
                }
                break;
            case MotionEvent.ACTION_UP:
                // These are synthetic events and there is no need to update internal values.
                if (mState == ScrollState.DRAGGING) {
                    setState(ScrollState.SETTLING);
                }
                break;
            ...
        }
        // Do house keeping.
        mLastDisplacement = mDisplacementY;
        mLastY = ev.getY();
        return true;
    }

可以看到VerticalPullDetector类有一个接口,还有一个枚举变量,并且AllAppsTransitionController实现了VerticalPullDetector的Listener接口。接下来我们就来看下这些接口方法的调用和状态的管理过程。在MotionEvent.ACTION_DOWN处理过程中,先记录下初始位置(mDownX,mDownY),在MotionEvent.ACTION_MOVE中计算移动的距离mDisplacementX,mDisplacementY,计算出滑动速度,然后调用setState()。setState方法的作用一是设置新的状态,二是根据状态回调AllAppsTransitionController的相应方法。最后调用reportDragging()从而调用AllAppsTransitionController的onDrag()方法。

packages\apps\Launcher3\src\com\android\launcher3\allapps\AllAppsTransitionController.java
    @Override
    public void onDragStart(boolean start) {
        mCaretController.onDragStart();
        cancelAnimation();
        mCurrentAnimation = LauncherAnimUtils.createAnimatorSet();
        mShiftStart = mAppsView.getTranslationY();
        preparePull(start);
    }

可以看到onDragStart()是一些初始化的操作,重新创建了一个mCurrentAnimation对象。比较简单,主要的还是onDrag()方法里面调用的setProgress()方法。

packages\apps\Launcher3\src\com\android\launcher3\allapps\AllAppsTransitionController.java
   /**
     * @param progress       value between 0 and 1, 0 shows all apps and 1 shows workspace
     */
    public void setProgress(float progress) {
        float shiftPrevious = mProgress * mShiftRange;
        mProgress = progress;
        float shiftCurrent = progress * mShiftRange;

        float workspaceHotseatAlpha = Utilities.boundToRange(progress, 0f, 1f);
        float alpha = 1 - workspaceHotseatAlpha;
        float interpolation = mAccelInterpolator.getInterpolation(workspaceHotseatAlpha);

        int color = (Integer) mEvaluator.evaluate(mDecelInterpolator.getInterpolation(alpha),
                mHotseatBackgroundColor, mAllAppsBackgroundColor);
        int bgAlpha = Color.alpha((int) mEvaluator.evaluate(alpha,
                mHotseatBackgroundColor, mAllAppsBackgroundColor));

        mAppsView.setRevealDrawableColor(ColorUtils.setAlphaComponent(color, bgAlpha));
        mAppsView.getContentView().setAlpha(alpha);
        mAppsView.setTranslationY(shiftCurrent);

        if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
            mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, -mShiftRange + shiftCurrent,
                    interpolation);
        } else {
            mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y,
                    PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent),
                    interpolation);
        }

        if (mIsTranslateWithoutWorkspace) {
            return;
        }
        mWorkspace.setWorkspaceYTranslationAndAlpha(
                PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), interpolation);

        if (!mDetector.isDraggingState()) {
            mContainerVelocity = mDetector.computeVelocity(shiftCurrent - shiftPrevious,
                    System.currentTimeMillis());
        }

        mCaretController.updateCaret(progress, mContainerVelocity, mDetector.isDraggingState());
        updateLightStatusBar(shiftCurrent);
    }

setProgress()方法还是比较好理解的,根据progress的值计算出相对应的颜色、透明度,偏移量,并且设置到相应的view上面。

最后,我们要分析的ACTION_UP事件了。在事件处理中调用setState(ScrollState.SETTLING),从而调用AllAppsTransitionController的onDragEnd()方法。

packages\apps\Launcher3\src\com\android\launcher3\allapps\AllAppsTransitionController.java
    public void onDragEnd(float velocity, boolean fling) {
        if (mAppsView == null) {
            return; // early termination.
        }
        if (fling) {//是否是快速滑动事件
            if (velocity < 0) { //表示向上滑动
                calculateDuration(velocity, mAppsView.getTranslationY());

                if (!mLauncher.isAllAppsVisible()) {
                    mLauncher.getUserEventDispatcher().logActionOnContainer(
                            LauncherLogProto.Action.FLING,
                            LauncherLogProto.Action.UP,
                            LauncherLogProto.HOTSEAT);
                }
                mLauncher.showAppsView(true /* animated */,
                        false /* updatePredictedApps */,
                        false /* focusSearchBar */);
            } else {
                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
                mLauncher.showWorkspace(true);
            }
            // snap to top or bottom using the release velocity
        } else {
            Log.i(TAG,"onDragEnd mShiftRange / 2 =" + mShiftRange / 2+",getTranslationY="+ mAppsView.getTranslationY());
            if (mAppsView.getTranslationY() > mShiftRange / 2) { //说明在一半以下
                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
                mLauncher.showWorkspace(true);
            } else {
                calculateDuration(velocity, Math.abs(mAppsView.getTranslationY()));
                if (!mLauncher.isAllAppsVisible()) {
                    mLauncher.getUserEventDispatcher().logActionOnContainer(
                            LauncherLogProto.Action.SWIPE,
                            LauncherLogProto.Action.UP,
                            LauncherLogProto.HOTSEAT);
                }
                mLauncher.showAppsView(true, /* animated */
                        false /* updatePredictedApps */,
                        false /* focusSearchBar */);
            }
        }
    }

函数一开始先判断是否是快速滑动事件,如果是就看是否已经处于应用列表界面,如果不是就调用Launcher的showAppsView()方法显示应用列表,否则调用showWorkspace()来显示workspace。其中calculateDuration()是根据当前距离计算出动画的执行时间mAnimationDuration。如果不是快速滑动事件则判断当前滑动的距离是否滑过了一半,如果是则触发相应逻辑,之后走的流程类似。我们以滑动打开应用列表来分析,滑动显示workspace走的类似的流程,还请大家自己分析。showAppsView()中会调用showAppsOrWidgets方法。

packages\apps\Launcher3\src\com\android\launcher3\Launcher.java
    private boolean showAppsOrWidgets(State toState, boolean animated, boolean focusSearchBar) {
        ...
        if (toState == State.APPS) {
            mStateTransitionAnimation.startAnimationToAllApps(mWorkspace.getState(), animated,focusSearchBar);
        } else {
            mStateTransitionAnimation.startAnimationToWidgets(mWorkspace.getState(), animated);
        }
        ...
    }

packages\apps\Launcher3\src\com\android\launcher3\LauncherStateTransitionAnimation.java
    public void startAnimationToAllApps(final Workspace.State fromWorkspaceState,
            final boolean animated, final boolean startSearchAfterTransition) {
        ...
        int animType = CIRCULAR_REVEAL;
        if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
            animType = PULLUP;
        }
        // Only animate the search bar if animating from spring loaded mode back to all apps
        startAnimationToOverlay(fromWorkspaceState,
                Workspace.State.NORMAL_HIDDEN, buttonView, toView, animated, animType, cb);
    }

很显然我们是到APPS界面,toState肯定等于State.APPS也就是会进入startAnimationToOverlay()逻辑。

packages\apps\Launcher3\src\com\android\launcher3\LauncherStateTransitionAnimation.java
    private void startAnimationToOverlay(
            final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState,
            final View buttonView, final BaseContainerView toView,
            final boolean animated, int animType, final PrivateTransitionCallbacks pCb) {
        final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
        ...
        final View contentView = toView.getContentView();
        playCommonTransitionAnimations(toWorkspaceState, fromView, toView,
              animated, initialized, animation, layerViews);
        ...
        if (animType == CIRCULAR_REVEAL) {
            ...
        } else if (animType == PULLUP) {
            ...
            boolean shouldPost = mAllAppsController.animateToAllApps(animation, revealDurationSlide);
            ...
            final AnimatorSet stateAnimation = animation;
            final Runnable startAnimRunnable = new Runnable() {
                public void run() {
                    // Check that mCurrentAnimation hasn't changed while
                    // we waited for a layout/draw pass
                    if (mCurrentAnimation != stateAnimation)
                        return;

                    dispatchOnLauncherTransitionStart(fromView, animated, false);
                    dispatchOnLauncherTransitionStart(toView, animated, false);

                    // Enable all necessary layers
                    for (View v : layerViews.keySet()) {
                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
                            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
                        }
                        if (Utilities.ATLEAST_LOLLIPOP && v.isAttachedToWindow()) {
                            v.buildLayer();
                        }
                    }

                    toView.requestFocus();
                    stateAnimation.start();
                }
            };
            mCurrentAnimation = animation;
            if (shouldPost) {
                toView.post(startAnimRunnable);
            } else {
                startAnimRunnable.run();
            }
        }
    }

总的来说就是创建了一个AnimatorSet,然后先播放State状态改变的动画,再调用mAllAppsController.animateToAllApps()播放workspace到列表界面的动画。这其中会调用AllAppsTransitionController的animateToAllApps方法,里面有动画结束的监听,当动画结束时会调用Detector的finishedScrolling()方法更改State状态为ScrollState.IDLE。

好了,整个过程我们就分析了一遍了。由于本人知识水平有限,难免有写的不对的地方,还请大家指正。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐