WearableListView的使用和一些思考

今年加盟了一家作手錶的公司,至此開啓了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是全屏顯示的。

那咱們還有幾個問題要解決

1.如何顯示頭部。

咱們注意到,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滑動而滑動了。

 

2.如何中間數據高亮顯示。

首先咱們仍是要看下官方文檔,咱們發現WearableListView已經提供好這樣的接口。

1 @Override
2     public void onCenterPosition(boolean animate) {
3         ............
4     }
5 
6     @Override
7     public void onNonCenterPosition(boolean animate) {
8         ............
9     }

貌似,到此咱們的問題已經解決差很少了,那如今咱們能夠實現需求了嗎,對了,滑動時的那些效果如何實現。

 

3.滑動效果的實現

在這個地方,我開發過程當中遇到了不少的坑,方案都改了幾回,在這我一一道來吧,淚崩了。

第一個版本:

我已經知道中間和非中間狀態的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)的偏離度進行操做便可。

 

一個重要的教訓

切忌對不熟的控件想固然的使用,

要儘量地的弄懂控件的原理,

若是文檔沒有看明白,那就去看源碼,

若是源碼太過於複雜,至少要多作些實驗。

相關文章
相關標籤/搜索