SystemUI 拖拽事件分析
求你指教我們怎樣數算自己的日子,好叫我們得著智慧的心。----詩篇90:12
之前寫過兩篇關於SystemUI的文章:
SystemUI之功能介紹和UI佈局實現
SystemUI之呈現流程
本篇分析下SystemUI 拖拽事件處理的過程。
他山之石可以攻玉,通過本篇的分析力求能觸控到Android團隊對複雜view的處理技巧,以便今後我們也能在自己的專案裡運用上這些技巧。
著重分析下面幾個知識點
自定義View的高效佈局方式,onMesure,onLayout—onDraw如何實現技巧onTouchEvent—onIntecept—onDispach如何運用,手勢監聽處理邏輯程式碼的封裝性
開胃小菜---點選事件
如果對SystemUI佈局結構不瞭解,請先參考之前的文章SystemUI之功能介紹和UI佈局實現 ,我們先挑個軟柿子捏捏,看看下圖示意的點選事件是如何處理的。
這裡寫圖片描述
在放上SystemUI的佈局圖
這裡主要分析兩塊:
點選頂部,如何控制狀態列伸縮
根據SystemUI的佈局圖,很容易找到點選事件入口是在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;
}
這裡使用屬性動畫在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中
@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);
}
關鍵的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拖拽已經佔據了大部分篇幅,如果在介紹框架跟設計,恐怕篇幅會又臭又長。自己能力跟精力有限,本篇只好草草收場。
寫作的過程糾結無比,想推倒重新再來,卻又不甘心放棄已經寫成的前半部分。所謂"食之無味,棄之可惜"。恐怕讀的人也感覺無趣。希望讀的有心人能多提些好的寫作建議,不甚感激。
相關文章
- 滑鼠拖拽事件事件
- drag &drop 拖拽事件事件
- drag & drop 拖拽事件事件
- 關於systemui wifi圖示更新過程分析SystemUIWiFi
- [深入SystemUI]-SystemUI_statusbar的啟動流程SystemUI
- Android8.1 SystemUI原始碼分析之 Notification流程AndroidSystemUI原始碼
- JS滑鼠事件完成元素拖拽(簡單-高階)JS事件
- OpenHarmony SystemUI開發記錄SystemUI
- javascript基礎(滑鼠事件拖拽,setCapture()方法)(三十六)JavaScript事件APT
- Android8.1 SystemUI原始碼分析之 電池時鐘重新整理AndroidSystemUI原始碼
- 等待事件分析事件
- 【漏洞分析】KaoyaSwap 安全事件分析事件
- 怎麼把 Excel 的拖拽分析功能搬到 WEB 上ExcelWeb
- [深入SystemUI]-瞭解recents的啟動流程(一)SystemUI
- 手遊拖拽動作的應用情況與分析總結
- View事件機制分析View事件
- 視覺化拖拽 UI 佈局之拖拽篇視覺化UI
- JavaScript拖拽效果JavaScript
- js拖拽技能JS
- React拖拽元件React元件
- 從面相過程的拖拽到物件導向的拖拽再到簡易的元件拖拽物件元件
- js拖拽:右下角拖拽改變圖片大小JS
- 詳解javascript拖拽(二)拖拽的應用及示例JavaScript
- 事件分發原始碼分析事件原始碼
- RAC全域性等待事件分析事件
- 002-DOM事件例項-實現一個可以拖拽的登陸視窗事件
- 【漏洞分析】ReflectionToken BEVO代幣攻擊事件分析事件
- 基於Vue實現拖拽升級(九宮格拖拽)Vue
- HTML5拖拽API實現vue樹形拖拽元件HTMLAPIVue元件
- 原生JS拖拽效果JS
- sortable 可拖拽元件元件
- 自定義拖拽效果
- canvas矩形拖拽效果Canvas
- 小程式拖拽排序排序
- JQuery UI 拖拽排序jQueryUI排序
- html5拖拽HTML
- Cypress實現拖拽
- 視覺化拖拽元件庫一些技術要點原理分析視覺化元件