今年加盟了一家作手錶的公司,至此開啓了androidwear(類)的開發之門。html
近日要作一個手錶上的List顯示,爲此也是花了不少的心思在List效果上,多日下來,有些心得。android
手錶上的List,它的靜止圖是這樣的git
他的動態圖是這樣的。github
明確需求ide
1.顯示一個list佈局
2.list有一個頭部測試
3.list一個屏幕上顯示三個數據,中間那個數據高亮放大,並顯示詳細信息動畫
4.list滑動時有顯示效果的變化ui
拿到這個需求,在沒有作過androidwear的狀況下,仍是以爲比較複雜的。首先先去研究了下androidwear的list顯示特色。this
根據官方文檔,咱們認識了androidwear的經常使用list,WearableListView。(WearableListView的相關基礎知識,詳見建立列表1.0.md 和建立列表1.1.md )
根據androidwear的官方文檔中的例子,咱們知道 WearableListView僅支持三個等高的數據顯示
爲了描述方便,下面的例子,咱們都假設Wearable是全屏顯示的。
那咱們還有幾個問題要解決
咱們注意到,WearableListView的第一個數據是從屏幕中間開始顯示的。這樣,List上面就有一個很大的空白空間,這個空白空間用來顯示一個標題(好比Setting)是很是合理的。
而爲了總體UI顯示的效果,這個頭部也須要隨着Listview的Scroll同步進行滑動效果的顯示。
咱們的佈局能夠將WearableListView和這個頭部(mImgRecordRl)放在一塊兒顯示,經過addOnLayoutChangeListener監聽佈局的變化
1 @Override 2 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int 3 oldTop, int oldRight, int oldBottom) { 4 if (v == mImgRecordRl) { 5 mInitialHeaderHeight = bottom - top; 6 mInitialHeaderHeight += ((ViewGroup.MarginLayoutParams) v.getLayoutParams()).topMargin; 7 8 } else if (v == listView) { 9 adjustHeaderTranslation(); 10 } 11 }
同時經過addOnScrollListener監聽WearableListView的Scroll狀態,在onScroll時不斷的根據mInitialHeaderHeight調整Header的Translation
1 listView.addOnScrollListener(new WearableListView.OnScrollListener() { 2 @Override 3 public void onScroll(int var1) { 4 adjustHeaderTranslation(); 5 //rect.setPressed(true); 6 } 7 8 @Override 9 public void onAbsoluteScrollChange(int var1) { 10 11 } 12 13 @Override 14 public void onScrollStateChanged(int var1) { 15 ........... 21 } 22 23 @Override 24 public void onCentralPositionChanged(int var1) { 25 26 } 27 });
而調整Header TranslationY的方法就在這個adjustHeaderTranslation()裏了。(邏輯仍是很簡單清晰的,就是經過剛纔的mInitialHeaderHeight以及Listview三平均高度的實際來作的計算)
1 private void adjustHeaderTranslation() { 2 int translation = 0; 3 if (listView.getChildCount() > 0) { 4 translation = listView.getCentralViewTop() - listView.getChildAt(0).getTop(); 5 } 6 float newTranslation = Math.min(Math.max(-mInitialHeaderHeight, -translation), 0); 7 int position = listView.getChildPosition(this.listView.getChildAt(0)); 8 if (position != 0 && newTranslation >= 0) { 9 return; 10 } 11 mImgRecordRl.setTranslationY(newTranslation); 12 }
這樣,咱們第一個問題就解決了,頭部能夠隨着list滑動而滑動了。
首先咱們仍是要看下官方文檔,咱們發現WearableListView已經提供好這樣的接口。
1 @Override 2 public void onCenterPosition(boolean animate) { 3 ............ 4 } 5 6 @Override 7 public void onNonCenterPosition(boolean animate) { 8 ............ 9 }
貌似,到此咱們的問題已經解決差很少了,那如今咱們能夠實現需求了嗎,對了,滑動時的那些效果如何實現。
在這個地方,我開發過程當中遇到了不少的坑,方案都改了幾回,在這我一一道來吧,淚崩了。
我已經知道中間和非中間狀態的API(見上面的問題2),並且需求上要求中間和非中間的佈局確實區別很大,那麼我乾脆就寫好兩個佈局,在onNonCenterPosition和onCenterPosition分別的visible和gone不就能夠了嗎。
因而,我就這樣作了,結果是滑動時總感受到處處跳轉。
通過分析,看來最好不要用兩個佈局,由於不管如何,兩個佈局就是兩個view,這樣的界面切換確定是很是生硬的。
咱們採用一個佈局,而後在onNonCenterPosition和onCenterPosition分別對佈局裏的元素進行translation scale alpha的操做。
咱們發現,這樣比兩個佈局要好一點,相對沒有生硬的感受。可是仍是有忽然跳轉的感受。
從網上找了一些例子,發現你們的作法就是對translation scale alpha的操做加一個動畫,這樣可能會看起來不是那麼的生硬。
可是咱們發現若是加一個動畫,若是在動畫沒有徹底完成的狀況下,我右滑到一半不退出返回的話(注:這是androidwear的設計,右滑就是走onDestroy,而若是沒有完成右滑動做,到一半返回,這樣activity生命週期並無發生變化),動畫中止,這樣整個界面就會靜止在動畫中止前的狀態,像卡死同樣,而這種操做實際上是很是常見的,這也是這個版本測試屢次提的問題。
因爲沒有找到合適的方向,咱們與設計師討論修改了方案,改成滑動時不變,中止滑動時,以動畫的方式中間元素變化。
再看看需求裏的效果,他有一箇中間逐漸變大,兩邊逐漸縮小的效果。
通過實驗發現,
1.listview的scroll過程,listitem並無scroll,這樣只能作到對listview的監聽,而沒法實現對listItem的監聽。所以我只能在WearableListView中的onTouchEvent中加一個對listItem的監聽,
2.可是我後面又發現onNonCenterPosition onCenterPosition是一直在調用並刷新界面的,這樣這個監聽後作的處理會和onNonCenterPosition/onCenterPosition衝突。另外,對listItem的監聽,座標的變化很難處理,由於牽扯到上下兩個item都處理的狀況,嘗試了各類處理方式都會衝突(好比ACTION_DOWN的時候,加flag,而後onNonCenterPosition/onCenterPosition時特殊處理;而後還有快速滑動的邏輯須要特殊處理)。
3.最難處理的是第二個版本遇到的問題,如何處理右滑到一半不退出返回的狀況。由於咱們要讓滑動時全部元素都顯示一個簡單信息,滑動即將中止的時候,中間元素顯示一個詳細信息。那麼滑動中止,我須要實現onTouchEvent中的ACTION_UP。可是右滑到一半不退出返回的狀況是不會發生ACTION_UP事件的,這樣就會出現和第二個版本那個動畫同樣的問題。
這個版本耗費了大量的時間,而且出現了大量的bug。 最終因爲bug已經不可控,我決定仍是推倒重來,回到最初的設計。
通過那麼多的嘗試,在這個版本中,我決定好好研究下WearableListView的源碼。
咱們仍是從onNonCenterPosition/onCenterPosition開始研究,咱們知道WearableListView是繼承。
下面的代碼是從WearableListView中截取出來的。
1 public static class ViewHolder extends android.support.v7.widget.RecyclerView.ViewHolder { 2 public ViewHolder(View itemView) { 3 super(itemView); 4 } 5 6 protected void onCenterProximity(boolean isCentralItem, boolean animate) { 7 if (this.itemView instanceof WearableListView.OnCenterProximityListener) { 8 WearableListView.OnCenterProximityListener item = (WearableListView 9 .OnCenterProximityListener) this.itemView; 10 if (isCentralItem) { 11 item.onCenterPosition(animate); 12 } else { 13 item.onNonCenterPosition(animate); 14 } 15 16 } 17 } 18 .................. 19 }
1 private void notifyChildrenAboutProximity(boolean animate) { 2 //onAllItemScroll(animate); 3 4 WearableListView.LayoutManager layoutManager = (WearableListView.LayoutManager) this 5 .getLayoutManager(); 6 int count = layoutManager.getChildCount(); 7 if (count != 0) { 8 int index = layoutManager.findCenterViewIndex(); 9 10 int position; 11 for (position = 0; position < count; ++position) { 12 View view = layoutManager.getChildAt(position); 13 WearableListView.ViewHolder listener = this.getChildViewHolder(view); 14 listener.onCenterProximity(position == index, animate); 15 16 }
1 private void onScroll(int dy) { 2 isScroll = true; 3 Iterator var2 = this.mOnScrollListeners.iterator(); 4 5 while (var2.hasNext()) { 6 WearableListView.OnScrollListener listener = (WearableListView.OnScrollListener) var2 7 .next(); 8 listener.onScroll(dy); 9 } 10 this.notifyChildrenAboutProximity(true); 11 12 }
1 android.support.v7.widget.RecyclerView.OnScrollListener onScrollListener = new android 2 .support.v7.widget.RecyclerView.OnScrollListener() { 3 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 4 5 if (newState == 0 && WearableListView.this.getChildCount() > 0) { 6 WearableListView.this.handleTouchUp((MotionEvent) null, newState); 7 } 8 9 Iterator var3 = WearableListView.this.mOnScrollListeners.iterator(); 10 11 while (var3.hasNext()) { 12 WearableListView.OnScrollListener listener = (WearableListView 13 .OnScrollListener) var3.next(); 14 listener.onScrollStateChanged(newState); 15 } 16 17 } 18 19 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 20 WearableListView.this.onScroll(dy); 21 } 22 }; 23 this.setOnScrollListener(onScrollListener);
從上面的源碼能夠看出,listView onScroll的時候,也就是listItem onNonCenterPosition/onCenterPosition(animate = true)的時候(注:onNonCenterPosition/onCenterPosition(animate = false)是第一次進入界面進行layout的時候,這不是本文重點,就再也不詳述)。這樣咱們就能夠確認,滑動時的效果就應該在onNonCenterPosition/onCenterPosition中處理。
看到這裏,咱們就明白了爲何在onNonCenterPosition/onCenterPosition中使用動畫會有卡頓的效果,由於滾動的時候一直產生新的動畫。
可是問題來了,onNonCenterPosition/onCenterPosition中沒有滑動距離的參數,咱們如何判斷當前ListItem到底滑了多少呢?
咱們再一次的研究所謂Center和NonCenter,到底在滑動的時候,誰是Center,誰是NonCenter呢?
咱們找到剛纔源碼中那個標紅的findCenterViewIndex
1 private int findCenterViewIndex() { 2 int count = this.getChildCount(); 3 int index = -1; 4 int closest = 2147483647; 5 int centerY = WearableListView.getCenterYPos(WearableListView.this); 6 7 for (int i = 0; i < count; ++i) { 8 View child = WearableListView.this.getLayoutManager().getChildAt(i); 9 int childCenterY = WearableListView.this.getTop() + WearableListView 10 .getCenterYPos(child); 11 int distance = Math.abs(centerY - childCenterY); 12 if (distance < closest) { 13 closest = distance; 14 index = i; 15 } 16 } 17 18 if (index == -1) { 19 throw new IllegalStateException("Can\'t find central view."); 20 } else { 21 return index; 22 } 23 }
這段代碼仔細一看,其實就是一個意思:若是ListView高360,則Y座標處在60-180之間的元素就是CenterPosition,另外兩個就是NonCenterPostion。
好了,有了這個原理性的知識,咱們的技術方案一會兒豁然開朗。
咱們原先需求是上下兩邊的元素在向中間滑動的過程當中,進行scale translation alpha的操做。而我開始就理解錯了,總覺得要對上中下三個的元素同時進行操做。
其實一旦上下兩邊的元素(原始Y座標爲0 240)進入60-180這個Y座標的範圍,他就自動變成了CenterPosition。這樣只須要針對CenterPosition的元素的Y座標相對於120的中心點的偏離度進行操做便可。
1 @Override 2 public void onCenterPosition(boolean animate) { 3 float scale = (Math.abs(getY() - getHeight())) / (getHeight() / 2); //這個咱們稱之爲偏離度 4 5 mNameTv.setScaleX(1.55f - 0.55f * scale); 6 mNameTv.setScaleY(1.55f - 0.55f * scale); 7 mNameTv.setTranslationY(-40.0f + 40.0f * scale); 8 if (scale < 0.3f) { 9 mDateTimeLl.setAlpha(1.0f - scale); 10 mDuarionLl.setAlpha(1.0f - scale); 11 } else { 12 mDateTimeLl.setAlpha(0.0f); 13 mDuarionLl.setAlpha(0.0f); 14 } 15 } 16 17 @Override 18 public void onNonCenterPosition(boolean animate) { 19 20 mNameTv.setScaleX(1.0f); 21 mNameTv.setScaleY(1.0f); 22 mNameTv.setTranslationY(0.0f); 23 mDateTimeLl.setAlpha(0.0f); 24 mDuarionLl.setAlpha(0.0f); 25 }
至此,咱們文章開頭的那個需求就真正的實現了。
最後,咱們總結一下WearableListView的相關注意事項:
1.WearableListView 屏幕下,僅能顯示三個數據元素 listitem的高度就是listview高度/3
2.WearableListView進入界面,第一個元素(就是第0個)顯示在中間位置,咱們能夠在第一個元素的上方加一個Header
3.WearableListView若是要實現中間高亮的效果,要在onNonCenterPosition/onCenterPosition中作處理
4.滑動狀態下,WearableListView的CenterPosition判斷標準是 listitem的Y座標處於 (listview.getY + listItem.getHeight/2) —— listview.getCenterY之間
5.onNonCenterPosition/onCenterPosition嚴禁使用動畫
6.滑動時元素變化的最優設計方案是:針對CenterPosition的元素的Y座標相對於(listview.getCenterY - listItem.getHeight/2)的偏離度進行操做便可。
一個重要的教訓:
切忌對不熟的控件想固然的使用,
要儘量地的弄懂控件的原理,
若是文檔沒有看明白,那就去看源碼,
若是源碼太過於複雜,至少要多作些實驗。