SystemUI 拖拽事件分析

来源:互联网 发布:棋牌类游戏开发源码 编辑:程序博客网 时间:2024/06/05 02:18

求你指教我们怎样数算自己的日子,好叫我们得着智慧的心。—-诗篇90:12

之前写过两篇关于SystemUI的文章:
SystemUI之功能介绍和UI布局实现
SystemUI之呈现流程
本篇分析下SystemUI 拖拽事件处理的过程。

他山之石可以攻玉,通过本篇的分析力求能触摸到Android团队对复杂view的处理技巧,以便今后我们也能在自己的项目里运用上这些技巧。
着重分析下面几个知识点

  • 自定义View的高效布局方式,onMesure,onLayout—onDraw如何实现技巧
  • onTouchEvent—onIntecept—onDispach如何运用,手势监听处理逻辑
  • 代码的封装性

开胃小菜—点击事件

如果对SystemUI布局结构不了解,请先参考之前的文章SystemUI之功能介绍和UI布局实现 ,我们先挑个软柿子捏捏,看看下图示意的点击事件是如何处理的。
这里写图片描述
在放上SystemUI的布局图
这里写图片描述
这里主要分析两块:

点击顶部,如何控制状态栏伸缩

根据SystemUI的布局图,很容易找到点击事件入口是在NotificationPanelView的onClick里。

@Overridepublic void onClick(View v) {        if (v == mHeader) {            onQsExpansionStarted();            if (mQsExpanded) {                flingSettings(0 /* vel */, false /* expand */, null, true /* isClick */);            } else if (mQsExpansionEnabled) {                EventLogTags.writeSysuiLockscreenGesture(                        EventLogConstants.SYSUI_TAP_TO_OPEN_QS,                        0, 0);                flingSettings(0 /* vel */, true /* expand */, null, true /* isClick */);            }      }}

主要的事件处理被封装在了flingSettings方法中,

private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable,            boolean isClick) {        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;        //忽略非主要代码        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);        if (isClick) {            animator.setInterpolator(mTouchResponseInterpolator);            animator.setDuration(368);        } else {            mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);        }        //忽略非主要代码        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {            @Override            public void onAnimationUpdate(ValueAnimator animation) {                setQsExpansion((Float) animation.getAnimatedValue());            }        });        animator.addListener(new AnimatorListenerAdapter() {            @Override            public void onAnimationEnd(Animator animation) {                mScrollView.setBlockFlinging(false);                mScrollYOverride = -1;                mQsExpansionAnimator = null;                if (onFinishRunnable != null) {                    onFinishRunnable.run();                }            }        });        animator.start();        mQsExpansionAnimator = animator;        mQsAnimatorExpand = expand;    }

这里使用属性动画在onAnimationUpdate回调里控制状态栏收缩,设置了addUpdateListener监听器监听动画执行过程中值的变化,同时设置AnimatorListenerAdapter监听动画结束。

Tips:
如果只需要监听动画的某一个事件,比如结束事件,应该设置AnimatorListenerAdapter监听器,这样就只用实现需要的事件,如果设置的是AnimatorListener监听器,那么就不得不全部复写onAnimationStart/onAnimationRepeat/onAnimationEnd等回调事件,即使你只想要监听其中的一个回调事件。

在onAnimationUpdate回调里,可以拿到状态栏的当前高度,再来看看
setQsExpansion((Float) animation.getAnimatedValue())的执行情况,该方法又调用setQsTranslation(height)方法,在其中调用了mQsContainer.setY(height - mQsContainer.getDesiredHeight() + getHeaderTranslation())
语句,这个也就是状态栏的伸缩实现。

顶部view里的设置、时钟小图标如何跟随变化

顶部view里内容的变换同样也是在NotificationPanelView的setQsExpansion方法中实现。

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java

private void setQsExpansion(float height) {        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);        mQsFullyExpanded = height == mQsMaxExpansionHeight;        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {            setQsExpanded(true);        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {            setQsExpanded(false);            if (mLastAnnouncementWasQuickSettings && !mTracking && !isCollapsing()) {                announceForAccessibility(getKeyguardOrLockScreenString());                mLastAnnouncementWasQuickSettings = false;            }        }        mQsExpansionHeight = height;        mHeader.setExpansion(getHeaderExpansionFraction());        setQsTranslation(height);        ...

先调用setQsExpanded(boolean expanded)方法,最终通过动态更改布局参数,达到顶部view的整体收缩和拉伸。
调用方法链如下:

setQsExpanded---->updateQsState---->StatusBarHeaderView.setExpanded---->StatusBarHeaderView.updateEverything---->StatusBarHeaderView.updateHeights.

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java

private void updateHeights() {        int height = mExpanded ? mExpandedHeight : mCollapsedHeight;        ViewGroup.LayoutParams lp = getLayoutParams();        if (lp.height != height) {            lp.height = height;            setLayoutParams(lp);        }    }

顶部view整体的收缩看完了,在关注下顶部View的一个细节---MaterialDesign风格的立体效果是如何实现的。
StatusBarHeaderView.setExpansion–>StatusBarHeaderView.setExpansion–>StatusBarHeaderView.setClipping

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java

private void setClipping(float height) {        mClipBounds.set(getPaddingLeft(), 0, getWidth() - getPaddingRight(), (int) height);        setClipBounds(mClipBounds);        invalidateOutline();    }

接着在分析内部小控件是如何变换的。同样从setExpansion看起。
setExpansion–>updateLayoutValues–>StatusBarHeaderView$LayoutValues.interpoloate–>applyLayoutValues
上面这条调用关系链都在StatusBarHeaderView里实现。看下interpoloate和applyLayoutValues方法

private static final class LayoutValues {    float timeScale = 1f;        float clockY;        float dateY;        ...        public void interpoloate(LayoutValues v1, LayoutValues v2, float t) {            timeScale = v1.timeScale * (1 - t) + v2.timeScale * t;            clockY = v1.clockY * (1 - t) + v2.clockY * t;            dateY = v1.dateY * (1 - t) + v2.dateY * t;            ...        }}
 private void applyLayoutValues(LayoutValues values) {        mTime.setScaleX(values.timeScale);        mTime.setScaleY(values.timeScale);        mClock.setY(values.clockY - mClock.getHeight());        mDateGroup.setY(values.dateY);

interpoloate方法先计算出缩放比例和透明度比例,然后在applyLayoutValues对控件做缩放处理。
以上分析完了状态栏伸缩的实现。其分析时用的代码基于Android5.0。Android7.0上SystemUI状态栏又发生了变化。

Android7.0上SystemUI拖拽实现

我们先看看Android7.0上SystemUI拖拽时的样子。
这里写图片描述

可以看到Android7.0上向上拖拽时,快捷小图标非常炫酷移动效果,下面来看看其如何实现。
根据SystemUI的布局图快捷小图标的父类视图为QSContainer,因此小图标的变化很可能在其中实现,查看其中的方法,在onFinishInflate()方法中有一个QSAnimator对象,onFinishInflate()方法在视图全部加载完成后会调用,而QSAnimator在SystemUI中是QuickSettingAnimator的缩写,这样看来动画的实现多半是在QSAnimator中实现。

frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java

    @Override    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,            int oldTop, int oldRight, int oldBottom) {        mQsPanel.post(mUpdateAnimators);    }

继续跟踪mUpdateAnimators来到了updateAnimators(),

private void updateAnimators() {    //...    for (QSTile<?> tile : tiles) {        //...        if (count < mNumQuickTiles && mAllowFancy) {                //...                    // Move the quick tile right from its location to the new one.                translationXBuilder.addFloat(quickTileView, "translationX", 0, xDiff);                translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff);                // Counteract the parent translation on the tile. So we have a static base to                // animate the label position off from.                firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0);                // Move the real tile's label from the quick tile position to its final                // location.                translationXBuilder.addFloat(label, "translationX", -xDiff, 0);                translationYBuilder.addFloat(label, "translationY", -yDiff, 0);                //...        }    }    if (mAllowFancy) {        //...        PathInterpolatorBuilder interpolatorBuilder = new PathInterpolatorBuilder(0, 0, 0, 1);        translationXBuilder.setInterpolator(interpolatorBuilder.getXInterpolator());        translationYBuilder.setInterpolator(interpolatorBuilder.getYInterpolator());        mTranslationXAnimator = translationXBuilder.build();        mTranslationYAnimator = translationYBuilder.build();    }}

以上代码通过mNumQuickTiles来确定动画结束后小图标的个数,默认为5,可以同过对settings数据库中的sysui_qqs_count字段来配置,而mAllowFancy决定是否开启动画效果。
来看看将mNumQuickTiles设置成7,关闭mAllowFancy后的效果
这里写图片描述

Tips:
更改settings数据库中某个字段的值,可以用类似如下的快捷方式:
adb shell settings put secure sysui_qqs_count 7

以上我们理清了Android7.0上拖拽动画的实现过程。细节方面还有一些疑惑。

动画是如何动起来的

translationXBuilder是TouchAnimator类中的一个静态类Builder,其build()方法返回的是一个TouchAnimator对象。
frameworks/base/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java

public class TouchAnimator {        public static class Builder {            //...            public TouchAnimator build() {                return new TouchAnimator(mTargets.toArray(new Object[mTargets.size()]),                        mValues.toArray(new KeyframeSet[mValues.size()]),                        mStartDelay, mEndDelay, mInterpolator, mListener);            }        }}

TouchAnimator是对动画类的封装,而其内建的Builder又是对动画参数的配置,那么问题来了,build方法直接返回了一个TouchAnimator对象,并没有看到其start动画,动画的所有参数已经配置好了,其已经处于就绪状态,它在何处被start呢?
为了弄清楚translationXBuilder到底如何工作的,在回到updateAnimators方法中,看看
translationXBuilder.addFloat(quickTileView, "translationX", 0, xDiff);
到底做了什么。

public Builder addFloat(Object target, String property, float... values) {    add(target, KeyframeSet.ofFloat(getProperty(target, property, float.class), values));    return this;}

这里的getProperty是个什么鬼

private static Property getProperty(Object target, String property, Class<?> cls) {        if (target instanceof View) {            switch (property) {                case "translationX":                    return View.TRANSLATION_X;                case "translationY":                    return View.TRANSLATION_Y;                case "translationZ":                    return View.TRANSLATION_Z;                case "alpha":                    return View.ALPHA;                case "rotation":                    return View.ROTATION;                case "x":                    return View.X;                case "y":                    return View.Y;                case "scaleX":                    return View.SCALE_X;                case "scaleY":                    return View.SCALE_Y;            }        }        if (target instanceof TouchAnimator && "position".equals(property)) {            return POSITION;        }        return Property.of(target.getClass(), cls, property);}

这种用法还第一次见到,厉害了我的谷歌哥!

我们传入的是quickTileView,getProperty根据属性返回给了对应的View.TRANSLATION_X,接着KeyframeSet.ofFloat new出一个FloatKeyframeSet对象,最后传入的quickTileView对象被存放在mTargets list中,FloatKeyframeSet对象被存放在mValues list中。

view有了,动画属性也设置进来了,最后动画属性如何被设置到view上呢?原来动画设置被隐藏在FloatKeyframeSet中

@Overrideprotected void interpolate(int index, float amount, Object target) {    float firstFloat = mValues[index - 1];    float secondFloat = mValues[index];    mProperty.set((T) target, firstFloat + (secondFloat - firstFloat) * amount);}

关键的mProperty.set语句实际上就相当于:

View.TRANSLATION_X.set(view, 100f);

它的主要调用过程如下:

NotificationPanelView.updateQsExpansion---->QSContainer.setQsExpansion---->QSAnimator.setPosition(expansion)---->TouchAnimator.setPosition(position)---->mKeyframeSets[i].setValue(t, mTargets[i])---->mProperty.set((T) target, firstFloat + (secondFloat - firstFloat) * amount);

后记

本篇博文的前半部分实际上早几个月已经完成了,当时计划本篇重点要阐述SystemUI的主体框架以及其中精妙的代码设计。UI上的拖拽动画只是作为开胃小菜顺带入题用的。但计划总被各种事情打断,当前也早已经不负责SystemUI模块的问题了,UI拖拽已经占据了大部分篇幅,如果在介绍框架跟设计,恐怕篇幅会又臭又长。自己能力跟精力有限,本篇只好草草收场。

写作的过程纠结无比,想推倒重新再来,却又不甘心放弃已经写成的前半部分。所谓”食之无味,弃之可惜”。恐怕读的人也感觉无趣。希望读的有心人能多提些好的写作建议,不甚感激。

原创粉丝点击