这篇文章是实现InstaMaterial的一部分,今天我们将仔细讲解上篇文章中跳过的细节。也就是说我们实现的还是视频中9-13秒这个时间段。
这是今天这篇文章完成之后的最终效果(棒棒糖以及棒棒糖之前):是youtube视频,没法看。暂时连不上。。。
初始化
没有什么大书特书的,我们只需为feed卡片元素中的按钮(喜欢以及评论按钮)加上图标就可以了。这一步的代码提交在这里this commit.完了之后,我们还是不忙着去实现新的东西(新的UI元素),还需要、、、
修正bug以及优化性能
是啊,即便是小如InstaMaterial 这样的demo级应用,你也总能找到提升的空间。
Toolbar theme
首先我们遗漏了Toolbar的样式,这就是为什么menu按钮(Toolbar左边的按钮)的按下颜色是默认的深色。如下:
(作者的目的是做的和视频一模一样,即便是颜色的深浅,个人认为没必要这么较真是吧)
下载按钮(ToolBar右边的按钮)的按下颜色是浅色的,因为它是使用的带selector的自定义view,selector的定义如下
menu_item_view.xml
1 |
android:background="@drawable/btn_default_light" |
但是这只在Lollipop上有效果(这里翻译可能有误,原文是This inconsistency appears only in Android Lollipop ),解决的办法很简单。只需在activity_comments.xml 和activity_main.xml的ToolBar控件中加上一行代码:
1 |
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" |
注:在toolbar中是这样使用的:
1 2 3 4 5 6 7 8 |
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/tools" android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:elevation="@dimen/default_elevation" app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> |
这样Toolbar上的所有元素都将有着继承自Dark.ActionBar主题的样式。
顺便说下,如果你对android主题和样式的定义感兴趣,想知道他们的区别,这篇文章值得一读-Styling Views on Android (Without Going Crazy).
按照上面的做了之后,menu的按下效果看起来就是这个样子了(只在Lollipop 中):
RecyclerView 元素的预加载
另一个问题是在app启动之后feed列表的滚动不太顺滑。几乎每次在滚到第二个卡片的时候都有卡顿。幸好,造成这个问题的原因简单。RecyclerView (以及其他基于adapter的view,比如ListView、GridView等)使用了缓存机制重用子view(简而言之就是,系统只将屏幕可见范围之内的元素保存在内存中,在滚动的时候不断的重用这些内存中已经存在的view,而不是新建view)。
这个机制在我们这里会导致一个问题,启动应用之后,在屏幕可见范围内,我们只有一张卡片可见(估计作者的屏幕比较小),当我们滚动的时候,RecyclerView找不到可以重用的view了,它将创建一个新的,因此在滑动到第二个feed的时候就会有一定的延时,但是第二个feed之后的滚动是流畅的,因为这个时候RecyclerView已经有能重用的view了。
如何解决这个问题?
好在本例使用的是LinearLayoutManager ,因此很简单。只需重写getExtraLayoutSpace()方法。根据官方文档的描述getExtraLayoutSpace将返回LayoutManager应该预留的额外空间(显示范围之外,应该额外缓存的空间)。
1 2 3 4 5 6 |
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this) { @Override protected int getExtraLayoutSpace(RecyclerView.State state) { return 300; } }; |
下面是实际效果:
在重写getExtraLayoutSpace()之前
重写之后:
Feed 卡片上的按钮
下面就让我们开始卡片上那些按钮(目前只有喜欢和评论按钮)的工作吧.从视频中的效果来看是非常赞的,圆形扩散的selector
Android Lollipop中的Ripple效果
按钮所使用的Selector无非就是棒棒糖中介绍的Ripple效果-一种水波扩散效果。已经有无数关于Ripple效果的文章,这里就不赘述了,我只给出两篇文章的链接:
- Ripples – Part 1 and Ripples – Part 2 from Styling Android blog
- Implementing Material Design in Your Android app from official Android Developers Blog
我们项目中实现Ripple的方法是非常简单的:
在feed_item.xml中将下面的drawable作为按钮的背景
res/drawable-v21/btn_feed_action.xml
不要在L之前的设备上使用ripples效果
我承诺过我将实现概念视频中的所有效果,但是有时候,去做一件达不到预期的事情,还不如不做。在pre-21的设备上实现ripple就是一件达不到预期的事情。
我当然知道可以使用诸如mimic ripple effect 这样的库来兼容老设备,但是没有一个库达到了该有的效果。它们需要添加额外的代码(比如添加额外的布局来包裹),在Lollipop版本上无法使用原生的Ripple 效果,性能问题等等。
But why is so hard to copy Ripple effect into pre-21 Android?
但是为什么在pre-21的设备上复制Ripple 效果会这么难呢?
Ripple揭秘
在棒棒糖版本之前,整个UI都是在UI主线程中管理的。几乎每个人都知道ANR对话框,NetworkOnMainThreadException,我们也是知道“不要将耗时操作放在UI线程中,仅仅在UI线程中显示操作结果”这条黄金定律。一切都看似可行,除了那句“整个UI都是被UI主线程所管理的”。
随着app的布局日益复杂,UI需要更多的时间去绘制、测量。现在问题来了,如果我们的动画执行到一半,而开启了另外一个UI任务(比如为新的activity inflat布局),但是只有一个UI线程,那么动画将被停止。
Lollipop中所引入的Render线程将解决这个问题。Render线程通过将渲染分成两部分来解决,简单的来说就是:我们有一个被UI线程创建的动画单元的列表,这些动画将被甩到独立的render线程当中。因此在执行开销较大的UI操作的时候,动画也能继续下去。
这就是ripple效果的工作原理。他们是在render线程中执行的。所以不会被打开新的activity这样的操作打断。
所以没法在pre-21的安卓系统上完全实现ripple效果。
老版本上的兼容“ripple”效果
因为不能在pre-21上实现ripple,那么我们就实现类似的效果就可以了。我们创建了一个带进入退出渐变的圆形的selector,尽管没有ripple那么花哨,但是看起来还是可以.
res/drawable/btn_feed_action.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="utf-8"?> <!--drawable/btn_feed_action.xml--> <selector xmlns:android="http://schemas.android.com/apk/res/android" android:enterFadeDuration="200" android:exitFadeDuration="200"> <item android:state_pressed="false"> <shape android:shape="oval"> <solid android:color="@android:color/transparent" /> </shape> </item> <item android:state_pressed="true"> <shape android:shape="oval"> <solid android:color="#6621425d" /> </shape> </item> </selector> |
评论发布按钮
评论发布按钮非常有趣。正如你在视频中看到的,按钮可以在两个状态之间做简单的动画切换,并且点击的时候有ripple效果。
注:评论发布按钮对应的类是SendCommentButton.java,这一节中讲解的内容都是在这个类中。结合代码更容易看明白。
关于这个按钮SendCommentButton,我将用到下面几个android元素:
ViewAnimator:作为SendCommentButton的基类,它有一个对我们非常有用的特性,可以设置子view切换时的进入和退出效果。如果你对ViewAnimator很陌生,其直接子类ViewFlipper, ViewSwitcher或间接子类ImageSwitcher, TextSwitcher应该很熟悉。
自定义view:包括xml以及inflate xml的代码
<merge>标签消除冗余的view
实现
好了,现在来开始实现,从动画开始。实际上我们需要四个动画,发送状态两个(进入和退出),完成状态两个(进入和退出,不过是相反的方向):
发送状态:
滑出顶端
从顶端滑入
完成状态:
从底部滑入
滑出底部
现在来实现button的布局:
因为ViewAnimator本身是一个FrameLayout,因此需要使用<merge>标签来减少了一层view。
最后,我们只有一个需求了,当button切换到完成状态之后,隔两秒它会自动切换回去。
代码很简单:
init()方法(29行)将前面创建的布局inflate给了ViewAnimator。顺便可以去看下这篇文章proper Layout Inflation,里面讲解了些很可能被你忽略的细节。
为了防止在activity结束的时候按钮的状态还没有切换回来,onDetachedFromWindow()去掉了回调方法revertStateRunnable()。
其余的都非常简单,通过setInAnimation() and setOutAnimation()两个方法来实现进入和退出动画。
好了,刚刚我们完成了SendCommentButton 的实现。注:这部分最好根据文中提到的变量名方法名对照代码理解。
selector
和feed中的操作按钮一样,我们需要为SendCommentButton准备两个selector。棒棒糖的设备我们使用ripple效果。pre-21的设备我们制造出标准的按下效果和阴影效果就可以了,就像在第一篇文章中对浮动操作按钮的做法。
下面是两种xml的代码:
res/drawable-v21/btn_send_comment.xml:
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="utf-8"?> <!--drawable-v21/btn_send_comment.xml--> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="#ffffff"> <item> <shape android:shape="rectangle"> <solid android:color="@color/btn_send_normal" /> </shape> </item> </ripple> |
btn_send_comment_v21.xml hosted with ❤ by GitHub
res/drawable/btn_send_comment.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?xml version="1.0" encoding="utf-8"?> <!--drawable/btn_send_comment.xml--> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="false"> <layer-list> <item android:bottom="0dp" android:left="2dp" android:right="0dp" android:top="2dp"> <shape android:shape="rectangle"> <solid android:color="@color/fab_color_shadow" /> <corners android:radius="2dp" /> </shape> </item> <item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp"> <shape android:shape="rectangle"> <solid android:color="@color/btn_send_normal" /> <corners android:radius="2dp" /> </shape> </item> </layer-list> </item> <item android:state_pressed="true"> <shape android:bottom="0dp" android:left="2dp" android:right="0dp" android:shape="rectangle" android:top="2dp"> <solid android:color="@color/btn_send_pressed" /> <corners android:radius="2dp" /> </shape> </item> </selector> |
btn_send_comment.xml hosted with ❤ by GitHub
错误振动提示
最后,我们将增加一个视频中没有出现的效果-当发送评论时如果输入框中没有内容,则播放振动动画。下面是效果图:
Implementation is pretty simple – we have to create shake animation and custom CycleInterpolator for repeating this animation. Everything is in a few lines of code:
res/anim/shake_error.xml:
实现很简单-创建一个振动动画并使用自定义的CycleInterpolator插值器来重复播放这个动画,只有几行代码:
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="300" android:fromXDelta="0%" android:interpolator="@anim/cycle_2" android:toXDelta="2%" /> |
shake_error.xml hosted with ❤ by GitHub
res/anim/cycle_2.xml:
1 2 3 |
<?xml version="1.0" encoding="utf-8"?> <cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android" android:cycles="2"/> |