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拖拽已经占据了大部分篇幅,如果在介绍框架跟设计,恐怕篇幅会又臭又长。自己能力跟精力有限,本篇只好草草收场。
写作的过程纠结无比,想推倒重新再来,却又不甘心放弃已经写成的前半部分。所谓”食之无味,弃之可惜”。恐怕读的人也感觉无趣。希望读的有心人能多提些好的写作建议,不甚感激。
- SystemUI 拖拽事件分析
- systemui 分析
- SystemUI代码分析
- android SystemUI 流程分析
- Android4.1Systemui分析
- Android SystemUI分析
- android4 SystemUI 流程分析
- Android 4.4 SystemUi分析
- android SystemUI 流程分析
- SystemUI源码分析
- SystemUi启动分析
- android SystemUI 流程分析
- SystemUI源码分析
- Android N SystemUI分析
- SystemUI架构分析
- android6.0 SystemUI之快捷设置区域QSPanel及点击事件流程分析
- android6.0 SystemUI之快捷设置区域QSPanel及点击事件流程分析
- Android SystemUI源码分析(一)
- 世界黑客大赛:苹果Mac潜伏近30年“骨灰级”漏洞
- 曾潜心研究养猪的丁磊,又悄悄做起了微商
- 腾讯云与企鹅智库联合预测直播行业:爆发后将迎来沉淀,专业化生产是趋势
- 台积电在美建厂遇到麻烦了,3nm工艺要黄?
- 存储黑科技!腾讯、阿里双双宣布使用英特尔Optane SSD
- SystemUI 拖拽事件分析
- Python socket programming
- C++类型转换运算符
- 【笔记】软件工程的人员方面
- JAVA设计模式-Observer模式
- 苹果宣布在上海和苏州设立研发中心,承诺中国研发投35亿元
- 刷新颜值高峰 vivo Xplay6磨砂黑版公布
- 英特尔为何要高调收购辅助驾驶明星企业Mobileye? | 本周专栏精选
- 大文娱板块开启联动 阿里游戏正式转守为攻