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)的偏离度进行操做便可。

 

一个重要的教训

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

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

若是文档没有看明白,那就去看源码,

若是源码太过于复杂,至少要多作些实验。

相关文章
相关标签/搜索