求你指教我們?cè)鯓訑?shù)算自己的日子骄崩,好叫我們得著智慧的心。----詩(shī)篇90:12
之前寫(xiě)過(guò)兩篇關(guān)于SystemUI的文章:
SystemUI之功能介紹和UI布局實(shí)現(xiàn)
SystemUI之呈現(xiàn)流程
本篇分析下SystemUI 拖拽事件處理的過(guò)程葛峻。
他山之石可以攻玉锹雏,通過(guò)本篇的分析力求能觸摸到Android團(tuán)隊(duì)對(duì)復(fù)雜view的處理技巧,以便今后我們也能在自己的項(xiàng)目里運(yùn)用上這些技巧术奖。
著重分析下面幾個(gè)知識(shí)點(diǎn)
自定義View的高效布局方式,onMesure,onLayout—onDraw如何實(shí)現(xiàn)技巧onTouchEvent—onIntecept—onDispach如何運(yùn)用礁遵,手勢(shì)監(jiān)聽(tīng)處理邏輯代碼的封裝性
開(kāi)胃小菜---點(diǎn)擊事件
如果對(duì)SystemUI布局結(jié)構(gòu)不了解,請(qǐng)先參考之前的文章SystemUI之功能介紹和UI布局實(shí)現(xiàn) 采记,我們先挑個(gè)軟柿子捏捏佣耐,看看下圖示意的點(diǎn)擊事件是如何處理的。
這里寫(xiě)圖片描述
在放上SystemUI的布局圖
這里主要分析兩塊:
點(diǎn)擊頂部唧龄,如何控制狀態(tài)欄伸縮
根據(jù)SystemUI的布局圖兼砖,很容易找到點(diǎn)擊事件入口是在NotificationPanelView的onClick里。
@Override
public 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;
}
這里使用屬性動(dòng)畫(huà)在onAnimationUpdate回調(diào)里控制狀態(tài)欄收縮讽挟,設(shè)置了addUpdateListener監(jiān)聽(tīng)器監(jiān)聽(tīng)動(dòng)畫(huà)執(zhí)行過(guò)程中值的變化,同時(shí)設(shè)置AnimatorListenerAdapter監(jiān)聽(tīng)動(dòng)畫(huà)結(jié)束丸冕。
Tips:
如果只需要監(jiān)聽(tīng)動(dòng)畫(huà)的某一個(gè)事件耽梅,比如結(jié)束事件,應(yīng)該設(shè)置AnimatorListenerAdapter監(jiān)聽(tīng)器胖烛,這樣就只用實(shí)現(xiàn)需要的事件眼姐,如果設(shè)置的是AnimatorListener監(jiān)聽(tīng)器诅迷,那么就不得不全部復(fù)寫(xiě)onAnimationStart/onAnimationRepeat/onAnimationEnd等回調(diào)事件,即使你只想要監(jiān)聽(tīng)其中的一個(gè)回調(diào)事件。
在onAnimationUpdate回調(diào)里众旗,可以拿到狀態(tài)欄的當(dāng)前高度罢杉,再來(lái)看看
setQsExpansion((Float) animation.getAnimatedValue())的執(zhí)行情況,該方法又調(diào)用setQsTranslation(height)方法贡歧,在其中調(diào)用了mQsContainer.setY(height - mQsContainer.getDesiredHeight() + getHeaderTranslation())
語(yǔ)句滩租,這個(gè)也就是狀態(tài)欄的伸縮實(shí)現(xiàn)。
頂部view里的設(shè)置艘款、時(shí)鐘小圖標(biāo)如何跟隨變化
頂部view里內(nèi)容的變換同樣也是在NotificationPanelView的setQsExpansion方法中實(shí)現(xiàn)。
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);
...
先調(diào)用setQsExpanded(boolean expanded)方法沃琅,最終通過(guò)動(dòng)態(tài)更改布局參數(shù)哗咆,達(dá)到頂部view的整體收縮和拉伸。
調(diào)用方法鏈如下:
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整體的收縮看完了益眉,在關(guān)注下頂部View的一個(gè)細(xì)節(jié)---MaterialDesign風(fēng)格的立體效果是如何實(shí)現(xiàn)的晌柬。
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();
}
接著在分析內(nèi)部小控件是如何變換的。同樣從setExpansion看起郭脂。
setExpansion-->updateLayoutValues-->StatusBarHeaderView$LayoutValues.interpoloate-->applyLayoutValues
上面這條調(diào)用關(guān)系鏈都在StatusBarHeaderView里實(shí)現(xiàn)年碘。看下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方法先計(jì)算出縮放比例和透明度比例展鸡,然后在applyLayoutValues對(duì)控件做縮放處理屿衅。
以上分析完了狀態(tài)欄伸縮的實(shí)現(xiàn)。其分析時(shí)用的代碼基于Android5.0莹弊。Android7.0上SystemUI狀態(tài)欄又發(fā)生了變化涤久。
Android7.0上SystemUI拖拽實(shí)現(xiàn)
我們先看看Android7.0上SystemUI拖拽時(shí)的樣子。
可以看到Android7.0上向上拖拽時(shí)忍弛,快捷小圖標(biāo)非常炫酷移動(dòng)效果响迂,下面來(lái)看看其如何實(shí)現(xiàn)。
根據(jù)SystemUI的布局圖快捷小圖標(biāo)的父類(lèi)視圖為QSContainer细疚,因此小圖標(biāo)的變化很可能在其中實(shí)現(xiàn)蔗彤,查看其中的方法,在onFinishInflate()方法中有一個(gè)QSAnimator對(duì)象疯兼,onFinishInflate()方法在視圖全部加載完成后會(huì)調(diào)用然遏,而QSAnimator在SystemUI中是QuickSettingAnimator的縮寫(xiě),這樣看來(lái)動(dòng)畫(huà)的實(shí)現(xiàn)多半是在QSAnimator中實(shí)現(xiàn)吧彪。
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);
}
繼續(xù)跟蹤mUpdateAnimators來(lái)到了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();
}
}
以上代碼通過(guò)mNumQuickTiles來(lái)確定動(dòng)畫(huà)結(jié)束后小圖標(biāo)的個(gè)數(shù)啦鸣,默認(rèn)為5,可以同過(guò)對(duì)settings數(shù)據(jù)庫(kù)中的sysui_qqs_count字段來(lái)配置来氧,而mAllowFancy決定是否開(kāi)啟動(dòng)畫(huà)效果诫给。
來(lái)看看將mNumQuickTiles設(shè)置成7香拉,關(guān)閉mAllowFancy后的效果
Tips:
更改settings數(shù)據(jù)庫(kù)中某個(gè)字段的值,可以用類(lèi)似如下的快捷方式:
adb shell settings put secure sysui_qqs_count 7
以上我們理清了Android7.0上拖拽動(dòng)畫(huà)的實(shí)現(xiàn)過(guò)程中狂。細(xì)節(jié)方面還有一些疑惑凫碌。
動(dòng)畫(huà)是如何動(dòng)起來(lái)的
translationXBuilder是TouchAnimator類(lèi)中的一個(gè)靜態(tài)類(lèi)Builder,其build()方法返回的是一個(gè)TouchAnimator對(duì)象。
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是對(duì)動(dòng)畫(huà)類(lèi)的封裝胃榕,而其內(nèi)建的Builder又是對(duì)動(dòng)畫(huà)參數(shù)的配置盛险,那么問(wèn)題來(lái)了,build方法直接返回了一個(gè)TouchAnimator對(duì)象勋又,并沒(méi)有看到其start動(dòng)畫(huà)苦掘,動(dòng)畫(huà)的所有參數(shù)已經(jīng)配置好了,其已經(jīng)處于就緒狀態(tài)楔壤,它在何處被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是個(gè)什么鬼
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);
}
這種用法還第一次見(jiàn)到,厲害了我的谷歌哥隙畜!
我們傳入的是quickTileView抖部,getProperty根據(jù)屬性返回給了對(duì)應(yīng)的View.TRANSLATION_X,接著KeyframeSet.ofFloat new出一個(gè)FloatKeyframeSet對(duì)象议惰,最后傳入的quickTileView對(duì)象被存放在mTargets list中慎颗,F(xiàn)loatKeyframeSet對(duì)象被存放在mValues list中。
view有了言询,動(dòng)畫(huà)屬性也設(shè)置進(jìn)來(lái)了哗总,最后動(dòng)畫(huà)屬性如何被設(shè)置到view上呢?原來(lái)動(dòng)畫(huà)設(shè)置被隱藏在FloatKeyframeSet中
@Override
protected 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);
}
關(guān)鍵的mProperty.set語(yǔ)句實(shí)際上就相當(dāng)于:
View.TRANSLATION_X.set(view, 100f);
它的主要調(diào)用過(guò)程如下:
NotificationPanelView.updateQsExpansion
---->QSContainer.setQsExpansion
---->QSAnimator.setPosition(expansion)
---->TouchAnimator.setPosition(position)
---->mKeyframeSets[i].setValue(t, mTargets[i])
---->mProperty.set((T) target, firstFloat + (secondFloat - firstFloat) * amount);
后記
本篇博文的前半部分實(shí)際上早幾個(gè)月已經(jīng)完成了倍试,當(dāng)時(shí)計(jì)劃本篇重點(diǎn)要闡述SystemUI的主體框架以及其中精妙的代碼設(shè)計(jì)讯屈。UI上的拖拽動(dòng)畫(huà)只是作為開(kāi)胃小菜順帶入題用的。但計(jì)劃總被各種事情打斷县习,當(dāng)前也早已經(jīng)不負(fù)責(zé)SystemUI模塊的問(wèn)題了涮母,UI拖拽已經(jīng)占據(jù)了大部分篇幅,如果在介紹框架跟設(shè)計(jì)躁愿,恐怕篇幅會(huì)又臭又長(zhǎng)叛本。自己能力跟精力有限,本篇只好草草收?qǐng)觥?/p>
寫(xiě)作的過(guò)程糾結(jié)無(wú)比彤钟,想推倒重新再來(lái)来候,卻又不甘心放棄已經(jīng)寫(xiě)成的前半部分。所謂"食之無(wú)味逸雹,棄之可惜"营搅≡菩恐怕讀的人也感覺(jué)無(wú)趣。希望讀的有心人能多提些好的寫(xiě)作建議转质,不甚感激园欣。