今年加盟了一家做手表的公司,至此开启了androidwear(类)的开发之门。

近日要做一个手表上的List显示,为此也是花了很多的心思在List效果上,多日下来,有些心得。

一.需求确定:

手表上的List,它的静止图是这样的

他的动态图是这样的。

明确需求

1.显示一个list

2.list有一个头部

3.list一个屏幕上显示三个数据,中间那个数据高亮放大,并显示详细信息

4.list滑动时有显示效果的变化

二.需求分析和相关技术

拿到这个需求,在没有做过androidwear的情况下,还是觉得比较复杂的。首先先去研究了下androidwear的list显示特点。

根据官方文档,我们认识了androidwear的常用list,WearableListView。(WearableListView的相关基础知识,详见创建列表1.0.md 和创建列表1.1.md

根据androidwear的官方文档中的例子,我们知道 WearableListView仅支持三个等高的数据显示

为了描述方便,下面的例子,我们都假设Wearable是全屏显示的。

那我们还有几个问题要解决

1.如何显示头部。

我们注意到,WearableListView的第一个数据是从屏幕中间开始显示的。这样,List上面就有一个很大的空白空间,这个空白空间用来显示一个标题(比如Setting)是非常合理的。

而为了整体UI显示的效果,这个头部也需要随着Listview的Scroll同步进行滑动效果的显示。

我们的布局可以将WearableListView和这个头部(mImgRecordRl)放在一起显示,通过addOnLayoutChangeListener监听布局的变化

     @Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int
oldTop, int oldRight, int oldBottom) {
if (v == mImgRecordRl) {
mInitialHeaderHeight = bottom - top;
mInitialHeaderHeight += ((ViewGroup.MarginLayoutParams) v.getLayoutParams()).topMargin; } else if (v == listView) {
adjustHeaderTranslation();
}
}

同时通过addOnScrollListener监听WearableListView的Scroll状态,在onScroll时不断的根据mInitialHeaderHeight调整Header的Translation

 listView.addOnScrollListener(new WearableListView.OnScrollListener() {
@Override
public void onScroll(int var1) {
adjustHeaderTranslation();
//rect.setPressed(true);
} @Override
public void onAbsoluteScrollChange(int var1) { } @Override
public void onScrollStateChanged(int var1) {
...........
} @Override
public void onCentralPositionChanged(int var1) { }
});

而调整Header TranslationY的方法就在这个adjustHeaderTranslation()里了。(逻辑还是很简单清晰的,就是通过刚才的mInitialHeaderHeight以及Listview三平均高度的实际来做的计算)

     private void adjustHeaderTranslation() {
int translation = 0;
if (listView.getChildCount() > 0) {
translation = listView.getCentralViewTop() - listView.getChildAt(0).getTop();
}
float newTranslation = Math.min(Math.max(-mInitialHeaderHeight, -translation), 0);
int position = listView.getChildPosition(this.listView.getChildAt(0));
if (position != 0 && newTranslation >= 0) {
return;
}
mImgRecordRl.setTranslationY(newTranslation);
}

这样,我们第一个问题就解决了,头部可以随着list滑动而滑动了。

2.如何中间数据高亮显示。

首先我们还是要看下官方文档,我们发现WearableListView已经提供好这样的接口。

 @Override
public void onCenterPosition(boolean animate) {
............
} @Override
public void onNonCenterPosition(boolean animate) {
............
}

貌似,到此我们的问题已经解决差不多了,那现在我们可以实现需求了吗,对了,滑动时的那些效果如何实现。

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中截取出来的。

 public static class ViewHolder extends android.support.v7.widget.RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
} protected void onCenterProximity(boolean isCentralItem, boolean animate) {
if (this.itemView instanceof WearableListView.OnCenterProximityListener) {
WearableListView.OnCenterProximityListener item = (WearableListView
.OnCenterProximityListener) this.itemView;
if (isCentralItem) {
item.onCenterPosition(animate);
} else {
item.onNonCenterPosition(animate);
} }
}
..................
}
 private void notifyChildrenAboutProximity(boolean animate) {
//onAllItemScroll(animate); WearableListView.LayoutManager layoutManager = (WearableListView.LayoutManager) this
.getLayoutManager();
int count = layoutManager.getChildCount();
if (count != 0) {
int index = layoutManager.findCenterViewIndex(); int position;
for (position = 0; position < count; ++position) {
View view = layoutManager.getChildAt(position);
WearableListView.ViewHolder listener = this.getChildViewHolder(view);
listener.onCenterProximity(position == index, animate); }
     private void onScroll(int dy) {
isScroll = true;
Iterator var2 = this.mOnScrollListeners.iterator(); while (var2.hasNext()) {
WearableListView.OnScrollListener listener = (WearableListView.OnScrollListener) var2
.next();
listener.onScroll(dy);
}
this.notifyChildrenAboutProximity(true); }
 android.support.v7.widget.RecyclerView.OnScrollListener onScrollListener = new android
.support.v7.widget.RecyclerView.OnScrollListener() {
public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == 0 && WearableListView.this.getChildCount() > 0) {
WearableListView.this.handleTouchUp((MotionEvent) null, newState);
} Iterator var3 = WearableListView.this.mOnScrollListeners.iterator(); while (var3.hasNext()) {
WearableListView.OnScrollListener listener = (WearableListView
.OnScrollListener) var3.next();
listener.onScrollStateChanged(newState);
} } public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
WearableListView.this.onScroll(dy);
}
};
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

         private int findCenterViewIndex() {
int count = this.getChildCount();
int index = -1;
int closest = 2147483647;
int centerY = WearableListView.getCenterYPos(WearableListView.this); for (int i = 0; i < count; ++i) {
View child = WearableListView.this.getLayoutManager().getChildAt(i);
int childCenterY = WearableListView.this.getTop() + WearableListView
.getCenterYPos(child);
int distance = Math.abs(centerY - childCenterY);
if (distance < closest) {
closest = distance;
index = i;
}
} if (index == -1) {
throw new IllegalStateException("Can\'t find central view.");
} else {
return index;
}
}

这段代码仔细一看,其实就是一个意思:如果ListView高360,则Y坐标处在60-180之间的元素就是CenterPosition,另外两个就是NonCenterPostion。

好了,有了这个原理性的知识,我们的技术方案一下子豁然开朗。

我们原先需求是上下两边的元素在向中间滑动的过程中,进行scale translation alpha的操作。而我开始就理解错了,总以为要对上中下三个的元素同时进行操作。

其实一旦上下两边的元素(原始Y坐标为0 240)进入60-180这个Y坐标的范围,他就自动变成了CenterPosition。这样只需要针对CenterPosition的元素的Y坐标相对于120的中心点的偏离度进行操作即可。

     @Override
public void onCenterPosition(boolean animate) {
float scale = (Math.abs(getY() - getHeight())) / (getHeight() / 2); //这个我们称之为偏离度 mNameTv.setScaleX(1.55f - 0.55f * scale);
mNameTv.setScaleY(1.55f - 0.55f * scale);
mNameTv.setTranslationY(-40.0f + 40.0f * scale);
if (scale < 0.3f) {
mDateTimeLl.setAlpha(1.0f - scale);
mDuarionLl.setAlpha(1.0f - scale);
} else {
mDateTimeLl.setAlpha(0.0f);
mDuarionLl.setAlpha(0.0f);
}
} @Override
public void onNonCenterPosition(boolean animate) { mNameTv.setScaleX(1.0f);
mNameTv.setScaleY(1.0f);
mNameTv.setTranslationY(0.0f);
mDateTimeLl.setAlpha(0.0f);
mDuarionLl.setAlpha(0.0f);
}

至此,我们文章开头的那个需求就真正的实现了。

最后,我们总结一下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)的偏离度进行操作即可。

 

一个重要的教训

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

要尽可能地的弄懂控件的原理,

如果文档没有看明白,那就去看源码,

如果源码太过于复杂,至少要多做些实验。

WearableListView的使用和一些思考的更多相关文章

  1. 领域驱动和MVVM应用于UWP开发的一些思考

    领域驱动和MVVM应用于UWP开发的一些思考 0x00 起因 有段时间没写博客了,其实最近本来是根据梳理的MSDN上的资料(UWP开发目录整理)有条不紊的进行UWP学习的.学习中有了心得体会或遇到了问 ...

  2. 关于面试题 Array.indexof() 方法的实现及思考

    这是我在面试大公司时碰到的一个笔试题,当时自己云里雾里的胡写了一番,回头也曾思考过,最终没实现也就不了了之了. 昨天看到有网友说面试中也碰到过这个问题,我就重新思考了这个问题的实现方法. 对于想进大公 ...

  3. 关于 CSS 反射倒影的研究思考

    原文地址:https://css-tricks.com/state-css-reflections 译者:nzbin 友情提示:由于演示 demo 的兼容性,推荐火狐浏览.该文章篇幅较长,内容庞杂,有 ...

  4. 关于.NET参数传递方式的思考

    年关将近,整个人已经没有了工作和写作的激情,估计这个时候很多人跟我差不多,该相亲的相亲,该聚会喝酒的聚会喝酒,总之就是没有了干活的心思(我有很多想法,但就是叫不动我的手脚,所以我只能看着别人在做我想做 ...

  5. 使用NUnit为游戏项目编写高质量单元测试的思考

    0x00 单元测试Pro & Con 最近尝试在我参与的游戏项目中引入TDD(测试驱动开发)的开发模式,因此单元测试便变得十分必要.这篇博客就来聊一聊这段时间的感悟和想法.由于游戏开发和传统软 ...

  6. OpenGL shader 中关于顶点坐标值的思考

    今天工作中需要做一个事情: 在shader内部做一些空间距离上的计算,而且需要对所有的点进行计算,符合条件的显示,不符合条件的点不显示. 思路很简单,在vertex shader内知道顶点坐标,进行计 ...

  7. 关于领域驱动设计(DDD)中聚合设计的一些思考

    关于DDD的理论知识总结,可参考这篇文章. DDD社区官网上一篇关于聚合设计的几个原则的简单讨论: 文章地址:http://dddcommunity.org/library/vernon_2011/, ...

  8. 关于bug分析与异常处理的一些思考

    前言:工作三年了,工作内容主要是嵌入式软件开发和维护,用的语言是C,毕业后先在一家工业自动化控制公司工作两年半,目前在一家医疗仪器公司担任嵌入式软件开发工作.软件开发中,难免不产生bug:产品交付客户 ...

  9. 【数据库】_由2000W多条开房数据引发的思考、实践----给在校生的一个真实【练耙场】,同学们,来开始一次伟大的尝试吧。

      ×   缘起---闲逛博客园 前几天的时候,在某一QQ群看到一条消息“XXX酒店开房XXXBTXX迅雷BT下载”,当时是一目十行的心态浏览,目光掠过时, 第一反应我想多了~以为是XX种子(你懂的~ ...

随机推荐

  1. 专家解读Linux操作系统内核中的GCC特性

    专家解读Linux操作系统内核中的GCC特性   Linux内核使用GNU Compiler Collection (GCC)套件的几个特殊功能.这些功能包括提供快捷方式和简化以及向编译器提供优化提示 ...

  2. Hadoop完全分布式集群安装

    转载请注明原地址,谢谢! 本文目的是教大家配置Hadoop的完全分布式的集群,除了完全分布式还有两种分别是单节点和伪分布式部署.伪分布式只需要一台虚拟机,配置的东西也相对较少,大多用作代码调试,大家稍 ...

  3. 【技术贴】7-zip 7z关联右键菜单后右键不弹出菜单的解决办法

    解决7z,7zip右键菜单 失效 右键菜单 无法弹出右键菜单 不正常 右键菜单 sb等各种疑难杂症. 1.首先先去7z的选项里面把右键关联给设置了. 用的好好的7z,正吃着火锅唱着歌忽然发现右键单文件 ...

  4. 【Java】对Web Service的理解

    WSDL(Web Service Description Language)是描述Web Service的语言. 你会怎样向别人介绍你的Web service有什么功能,以及每个函数调用时的参数呢?你 ...

  5. MySql的卸载问题

    windows下mysql的卸载: 彻底卸载Mysql的方法:   (1),先在服务(开始——>控制面板——>管理工具——>服务)里停掉MySQL的服务.打开控制面板-添加删除程序, ...

  6. 主席树:HDU 4417 Super Mario

    Super Mario Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  7. Codeforces 715B & 716D Complete The Graph 【最短路】 (Codeforces Round #372 (Div. 2))

    B. Complete The Graph time limit per test 4 seconds memory limit per test 256 megabytes input standa ...

  8. ink_test

  9. iOS __block用法

    没有__block qualifier的primitive c types会直接在创建block的时候被capture到block里.有__block qualifier的话,会在调用block的时候 ...

  10. PHP 生命周期,Opcode 缓存。

    1.php 执行的生命周期. 用户发出请求---->.php--->词典扫描--->解析--->创建Opcode--->处理opcode--->响应 这就是php的 ...