NotificationStackScrollLayout 刷新过程

本文将以左右滑动去除一条通知为例,对 NotificationStackScrollLayout 的刷新过程进行分析。

一. 滑动事件分发处理

首先Touch事件经过层层传递到达 NotificationStackScrollLayout 的onInterceptTouchEvent ():

public boolean onInterceptTouchEvent(MotionEvent ev) {
        ......
            if (mScrollView.onInterceptTouchEvent(ev)) {
                .......
            expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
        ......
            scrollWantsIt = onInterceptTouchEventScroll(ev);
        ......
            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
        }
        return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev);
    }

可以看到上述代码中分别对mScrollView,mExpandHelper,mSwipeHelper以及NotificationStackScrollLayout父类的onInterceptTouchEvent进行调用判断,
在以滑动去除一条通知为例时,MotionEvent的down事件传递进来,mScrollView,mExpandHelper,mSwipeHelper以及
NotificationStackScrollLayout父类的onInterceptTouchEvent()均返回false。在MotionEvent的move事件初次传递进来时,以上4处onInterceptTouchEvent()仍全部返回false。
但在多个move事件进入后,mSwipeHelper.onInterceptTouchEvent(ev);将返回true。使得此NotificationStackScrollLayout.onInterceptTouchEvent()的返回值此时为true,事件得以传递至
NotificationStackScrollLayout的onTouchEvent().

但是为什么一开始的down事件及前面几次的move事件mSwipeHelper.onInterceptTouchEvent(ev)返回false,而在多个move事件后mSwipeHelper.onInterceptTouchEvent(ev) 又变为true呢?
看看mSwipeHelper.onInterceptTouchEvent():

public boolean onInterceptTouchEvent(final MotionEvent ev) {
        final int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mTouchAboveFalsingThreshold = false;
                mDragging = false;
                mLongPressSent = false;
                mCurrView = mCallback.getChildAtPosition(ev);
                mVelocityTracker.clear();
                if (mCurrView != null) {
                    mCurrAnimView = mCallback.getChildContentView(mCurrView);
                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
                    mVelocityTracker.addMovement(ev);
                    mInitialTouchPos = getPos(ev);
                    ......
                }
                break;

            case MotionEvent.ACTION_MOVE:
                if (mCurrView != null && !mLongPressSent) {
                    mVelocityTracker.addMovement(ev);
                    float pos = getPos(ev);
                    float delta = pos - mInitialTouchPos;
                    if (Math.abs(delta) > mPagingTouchSlop) {
                        mCallback.onBeginDrag(mCurrView);//NotificationStackScrollLayout.onBeginDrag()
                        mDragging = true;
                        mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
                        removeLongPressCallback();
                    }
                }

                break;
        .......
        }
        return mDragging || mLongPressSent;
    }

看一下ACTION_DOWN中的这一句 mInitialTouchPos = getPos(ev); 此句将得到down事件对应的x位置。
在看ACTION_MOVE中的
float pos = getPos(ev);
float delta = pos - mInitialTouchPos;
首先得到此时move事件对应的x位置,通过此时x位置减去down事件中的初始位置,得到一个滑动偏移量delta。
再往下看, if (Math.abs(delta) > mPagingTouchSlop),判断delta的绝对值与mPagingTouchSlop的大小,
mPagingTouchSlop在SwipeHelper的构造函数中有如下赋值:

public SwipeHelper(int swipeDirection, Callback callback, Context context) {
        mCallback = callback;// NotificationStackScrollLayout
        ......
        mSwipeDirection = swipeDirection; //滑动方向
        mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();//触发滑动事件的最小距离
        ......
    }

而在NotificationStackScrollLayout的构造函数中:

public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        ......
        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext());
        ......
    }

getScaledPagingTouchSlop()获得触发移动事件的最小距离,即滑动偏移量大于mPagingTouchSlop时我们认为用户此时操作是一个滑动操作。
此时,执行mCallback.onBeginDrag(mCurrView)。这个mCallback是谁?
回头看SwipeHelper的构造函数,
即为NotificationStackScrollLayout。
NotificationStackScrollLayout.onBeginDrag():

public void onBeginDrag(View v) {
        setSwipingInProgress(true);
        mAmbientState.onBeginDrag(v);
        if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) {
            mDragAnimPendingChildren.add(v);
            mNeedsAnimation = true;
        }
        requestChildrenUpdate();
    }

此函数将执行mAmbientState.onBeginDrag(v),mNeedsAnimation = true;以及requestChildrenUpdate();
mNeedsAnimation的值后文会用到,一会再看。先看
requestChildrenUpdate();

private void requestChildrenUpdate() {
        if (!mChildrenUpdateRequested) {
            getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
            mChildrenUpdateRequested = true;
            invalidate();//此函数将最终导致视图重绘,onPreDraw及onDraw被调用
        }
    }

添加了一个OnPreDrawListener,随后再invalidate();去重绘view。
看看OnPreDrawListener:

private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
            = new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            updateChildren();
            mChildrenUpdateRequested = false;
            getViewTreeObserver().removeOnPreDrawListener(this);
            return true;
        }
    };

onPreDraw()在view重绘,ondraw()执行前被回调。
其主要执行的方法为updateChildren():

可以看到上文的mNeedsAnimation将在这里被用来做判断,同时这里将最终刷新所有的通知栏通知子视图,具体的刷新方式下文再进行分析。
整理一下思路,Touch事件从 NotificationStackScrollLayout 的onInterceptTouchEvent传递到mSwipeHelper.onInterceptTouchEvent,经过最短滑动距离判断,回调NotificationStackScrollLayout的
onBeginDrag(),在onBeginDrag()中mNeedsAnimation = true,并调用requestChildrenUpdate()重绘视图。经异步调用,
onPreDraw()被调用,最终updateChildren()刷新所有通知子视图。
以上这方面从整体上看起来,主要是对用户拖动通知进行事件判断并刷新视图,如果是滑动事件则继续向下传递。

由于是异步调用,当invalidate()执行后不会等待updateChildren()的刷新,直接返回到requestChildrenUpdate(),再到
onBeginDrag(),再到mSwipeHelper.onInterceptTouchEvent(),执行mDragging = true;
重置mInitialTouchPos为系统认为的滑动操作的起点。break。
return mDragging || mLongPressSent;
由于此时mDragging = true;所以return true;
与文章最开始的NotificationStackScrollLayout的onInterceptTouchEvent()返回true的情况对应起来。
NotificationStackScrollLayout的onInterceptTouchEvent()返回true。
NotificationStackScrollLayout的onTouchEvent将被调用。

public boolean onTouchEvent(MotionEvent ev) {
        ......
        if (!mIsBeingDragged
                && !mExpandingNotification
                && !mExpandedInThisMotion
                && !mOnlyScrollingInThisMotion) {
            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
        }
        return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
    }

这里最终调用了mSwipeHelper.onTouchEvent(ev);

public boolean onTouchEvent(MotionEvent ev) {
        ......
        mVelocityTracker.addMovement(ev);
        final int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_OUTSIDE:
            case MotionEvent.ACTION_MOVE:
                if (mCurrView != null) {
                    float delta = getPos(ev) - mInitialTouchPos;
                    float absDelta = Math.abs(delta);
                    if (absDelta >= getFalsingThreshold()) {
                        mTouchAboveFalsingThreshold = true;
                    }
                    ......
                    setTranslation(mCurrAnimView, delta);
                    updateSwipeProgressFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mCurrView != null) {
                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
                    float velocity = getVelocity(mVelocityTracker);
                    ......
                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
                            && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough)
                            && ev.getActionMasked() == MotionEvent.ACTION_UP;

                    if (dismissChild) {
                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
                    } else {
        ......
    }

可以看到在move的事件处理中,不断计算滑动偏移量absDelta,使用setTranslation()将偏移量设置给view,逐步移动view。
随后再调用updateSwipeProgressFromOffset(),

private void updateSwipeProgressFromOffset(View animView, boolean dismissable) {
        float swipeProgress = getSwipeProgressForOffset(animView);
        ......
                float alpha = swipeProgress;
                animView.setAlpha(getSwipeProgressForOffset(animView));
            }
        }
        invalidateGlobalRegion(animView);
    }

在updateSwipeProgressFromOffset()中更改了view的透明度,
并调用invalidateGlobalRegion(animView),重绘view。
滑动的过程中将产生多个move事件,经上述调用,不断移动,不断重绘,从而造成弹性滑动的显示效果。

二. 通知的移除

滑动以up事件结束,case MotionEvent.ACTION_UP:中未处理,不过也没有break,所以继续向下看:ACTION_CANCEL。
在其中先计算滑动速度velocity,再通过当前的滑动速度,滑动距离,判断当前view是否是一个将要去除的child,
如果是,将调用dismissChild();

public void dismissChild(final View view, float velocity, final Runnable endAction,
            long delay, boolean useAccelerateInterpolator, long fixedDuration) {
        final View animView = mCallback.getChildContentView(view);
        ......
        ObjectAnimator anim = createTranslationAnimation(animView, newPos);
        ......
        anim.setDuration(duration);
        if (delay > 0) {
            anim.setStartDelay(delay);
        }
        anim.addListener(new AnimatorListenerAdapter() {
            public void onAnimationEnd(Animator animation) {
                mCallback.onChildDismissed(view);//去除child的真正入口
                if (endAction != null) {
                    endAction.run();
                }
                animView.setLayerType(View.LAYER_TYPE_NONE, null);
            }
        });
        anim.addUpdateListener(new AnimatorUpdateListener() {
            public void onAnimationUpdate(ValueAnimator animation) {
                updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
            }
        });
        anim.start();
    }

可以看到这里将完成一个左右滑动的动画,同时设置onAnimationUpdate,在动画的每一帧都调用updateSwipeProgressFromOffset()
,不断移动并重绘view。在动画结束时调用onAnimationEnd(),执行NotificationStackScrollLayout.onChildDismissed()

public void onChildDismissed(View v) {
        ......
        mAmbientState.onDragFinished(v);
        .....
        veto.performClick();
        .....
 }

performClick()为模拟触发点击事件。我们看一下veto的onclick()在哪定义的。
找了一圈之后,发现它定义在BaseStatusBar中,

protected View updateNotificationVetoButton(View row, StatusBarNotification n) {
       ......
            vetoButton.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        // Accessibility feedback
                        v.announceForAccessibility(
                                mContext.getString(R.string.accessibility_notification_dismissed));
                        try {
                            mBarService.onNotificationClear(_pkg, _tag, _id, _userId);

     
       ......     
    }

可以看到这里最终去执行mBarService.onNotificationClear();
去对应的service中查看:
StatusBarManagerService.onNotificationClear:

public void onNotificationClear(String pkg, String tag, int id, int userId) {
      ......
            mNotificationDelegate.onNotificationClear(callingUid, callingPid, pkg, tag, id, userId);
      ......
}

mNotificationDelegate NotificationDelegate 是一个接口。
在NotificationManagerService对该接口进行匿名类复写

 private final NotificationDelegate mNotificationDelegate = new NotificationDelegate() {
     ......
     public void onNotificationClear(int callingUid, int callingPid,
                String pkg, String tag, int id, int userId) {
            cancelNotification(callingUid, callingPid, pkg, tag, id, 0,
                    Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE,
                    true, userId, REASON_DELEGATE_CANCEL, null);
     }
}


void cancelNotification(final int callingUid, final int callingPid,
            final String pkg, final String tag, final int id,
            final int mustHaveFlags, final int mustNotHaveFlags, final boolean sendDelete,
            final int userId, final int reason, final ManagedServiceInfo listener) {
                
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                ......
                mNotificationList.remove(index);
            }

最终通知在这里被移除掉。
以上第二部分主要判断用户的滑动事件是否是打算删除一条通知,并对被滑动的视图进行位置及透明度变化的动画,增强用户体验。而在通知动画结束后,去通知列表中真正的移除此条通知,在视图及数据中都完成移除。

三. 通知去除后NotificationStackScrollLayout的刷新

移除通知后NotificationStackScrollLayout重绘,重走onMeasure(),onLayout(),onPreDraw(),onDraw()方法。
当走到onPreDraw()时再次执行,执行updateChildren().

private void updateChildren() {
        mAmbientState.setScrollY(mOwnScrollY);
        mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
            applyCurrentState();
        } else {
            startAnimationToState();
        }
    }

本例为左右滑动,mOwnScrollY此时值为0。
mAmbientState是AmbientState的实例,其描述为: A global state to track all input states for the algorithm . 为了StackScrollAlgorithm ,追踪所有输入状态的全局状态。它将记录用户的输入轨迹,及NotificationStackScrollLayout 的部分属性,在下面的StackScrollAlgorithm计算时作为一个参数。
随后判断当前是否正在播放动画isCurrentlyAnimating(),以及 mNeedsAnimation的值。
在上文执行的onBeginDrag()中有一句mAmbientState.onBeginDrag(v);
而在onChildDismissed()中有一句mAmbientState.onDragFinished(v);
AmbientState.onBeginDrag()及AmbientState.onDragFinished():

public void onBeginDrag(View view) {        
        mDraggedViews.add(view);    
}    
public void onDragFinished(View view) {     
        mDraggedViews.remove(view);   
}

即在用户拖动的开始和结束时分别记录和移除的用户拖动的view。

mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
getStackScrollState()中通过当前的mCurrentStackScrollState与mAmbientState将计算得出一个新的mCurrentStackScrollState。
getStackScrollState():

public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {        
        StackScrollAlgorithmState algorithmState = mTempAlgorithmState;        
        resultState.resetViewStates(); 
        algorithmState.itemsInTopStack = 0.0f;       
        algorithmState.partialInTop = 0.0f;       
        algorithmState.lastTopStackIndex = 0;
        algorithmState.scrolledPixelsTop = 0;        
        algorithmState.itemsInBottomStack = 0.0f;        
        algorithmState.partialInBottom = 0.0f;
        float bottomOverScroll = ambientState.getOverScrollAmount(false 
      
        int scrollY = ambientState.getScrollY();
        
        scrollY = Math.*max*(0, scrollY);      
        algorithmState.scrollY = (int) (scrollY+ mCollapsedSize +bottomOverScroll);
       
        updateVisibleChildren(resultState, algorithmState);
        findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState);       
        updatePositionsForState(resultState, algorithmState, ambientState);      
        updateZValuesForState(resultState, algorithmState);
       
        handleDraggedViews(ambientState, resultState, algorithmState);
        updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);        
        updateClipping(resultState, algorithmState, ambientState);
        updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex());       
        getNotificationChildrenStates(resultState, algorithmState);   
}

计算得出一个新状态后我们继续向下看,updateChildren()中的mNeedsAnimation在上文执行onBeginDrag()时被置为true;
此时将执行 startAnimationToState();

private void startAnimationToState() {
        if (mNeedsAnimation) {
            generateChildHierarchyEvents();
            mNeedsAnimation = false;
        }
        if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState,
                    mGoToFullShadeDelay);
            mAnimationEvents.clear();
        } else {
            applyCurrentState();
        }
        mGoToFullShadeDelay = 0;
    }

首先生成子层次事件并mNeedsAnimation = false。随后经判断,走到startAnimationForEvents(),用以启动更新动画。

public void startAnimationForEvents(
            ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
            StackScrollState finalState, long additionalDelay) {
            ......
        for (int i = 0; i < childCount; i++) {
            ......
            final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
            ......
            startStackAnimations(child, viewState, finalState, i, -1 /* fixedDelay */);
        }
        
        if (!isRunning()) {
            // no child has preformed any animation, lets finish
            onAnimationFinished();
        }   
    }

最终在这里for循环为每一个child view执行状态改变的动画。
在动画播放完毕时调用onAnimationFinished();

private void onAnimationFinished() {
        mHostLayout.onChildAnimationFinished();
        ......
    }

mHostLayout即为NotificationStackScrollLayout,
NotificationStackScrollLayout.onChildAnimationFinished():

public void onChildAnimationFinished() {
        requestChildrenUpdate();
        runAnimationFinishedRunnables();
        clearViewOverlays();
}

在这里再次执行requestChildrenUpdate();也将再次调用invalidate();onPreDraw();updateChildren();

private void updateChildren() {
        mAmbientState.setScrollY(mOwnScrollY);
        mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
            applyCurrentState();
        } else {
            startAnimationToState();
        }
    }

不过此时进入updateChildren()时mNeedsAnimation=false(在startAnimationToState中被置为false),走applyCurrentState();

private void applyCurrentState() {
        mCurrentStackScrollState.apply();
        ......
}

public void apply() {
        int numChildren = mHostView.getChildCount();
        for (int i = 0; i < numChildren; i++) {
            ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
            StackViewState state = mStateMap.get(child);
            if (!applyState(child, state)) {
                continue;
            }
            ......
        }
    }    

在这里for循环直接为各个child应用此状态。不过由于在上面startAnimationToState()已经通过动画应用过当前状态,此时执行
applyCurrentState()看起来并未有所改变。

在移除通知后,被移除的通知的位置将留下一处空白,利用
StackScrollAlgorithm计算AmbientState和CurrentStackScrollState,得到一个新的CurrentStackScrollState状态。随后,使用动画将当前CurrentStackScrollState状态调整到新的CurrentStackScrollState状态,调整的过程中其余通知的位置将被移动,填补被移除通知的位置空白。

到这里,一次左右滑动去除通知的刷新过程结束。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容