本文将以左右滑动去除一条通知为例,对 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状态,调整的过程中其余通知的位置将被移动,填补被移除通知的位置空白。
到这里,一次左右滑动去除通知的刷新过程结束。