InstaMaterial概念设计第三部分-feed卡片上的按钮、评论按钮

这篇文章是实现InstaMaterial的一部分,今天我们将仔细讲解上篇文章中跳过的细节。也就是说我们实现的还是视频中9-13秒这个时间段。

这是今天这篇文章完成之后的最终效果(棒棒糖以及棒棒糖之前):是youtube视频,没法看。暂时连不上。。。

初始化

没有什么大书特书的,我们只需为feed卡片元素中的按钮(喜欢以及评论按钮)加上图标就可以了。这一步的代码提交在这里this commit.完了之后,我们还是不忙着去实现新的东西(新的UI元素),还需要、、、

修正bug以及优化性能

是啊,即便是小如InstaMaterial 这样的demo级应用,你也总能找到提升的空间。

Toolbar theme

首先我们遗漏了Toolbar的样式,这就是为什么menu按钮(Toolbar左边的按钮)的按下颜色是默认的深色。如下:

Toolbar without styling

(作者的目的是做的和视频一模一样,即便是颜色的深浅,个人认为没必要这么较真是吧)

下载按钮(ToolBar右边的按钮)的按下颜色是浅色的,因为它是使用的带selector的自定义view,selector的定义如下

menu_item_view.xml

android:background="@drawable/btn_default_light"

但是这只在Lollipop上有效果(这里翻译可能有误,原文是This inconsistency appears only in Android Lollipop ),解决的办法很简单。只需在activity_comments.xml 和activity_main.xml的ToolBar控件中加上一行代码:

app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"

注:在toolbar中是这样使用的:

<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 中):

Toolbar with styling

RecyclerView 元素的预加载

另一个问题是在app启动之后feed列表的滚动不太顺滑。几乎每次在滚到第二个卡片的时候都有卡顿。幸好,造成这个问题的原因简单。RecyclerView (以及其他基于adapter的view,比如ListView、GridView等)使用了缓存机制重用子view(简而言之就是,系统只将屏幕可见范围之内的元素保存在内存中,在滚动的时候不断的重用这些内存中已经存在的view,而不是新建view)。

这个机制在我们这里会导致一个问题,启动应用之后,在屏幕可见范围内,我们只有一张卡片可见(估计作者的屏幕比较小),当我们滚动的时候,RecyclerView找不到可以重用的view了,它将创建一个新的,因此在滑动到第二个feed的时候就会有一定的延时,但是第二个feed之后的滚动是流畅的,因为这个时候RecyclerView已经有能重用的view了。

如何解决这个问题?

好在本例使用的是LinearLayoutManager ,因此很简单。只需重写getExtraLayoutSpace()方法。根据官方文档的描述getExtraLayoutSpace将返回LayoutManager应该预留的额外空间(显示范围之外,应该额外缓存的空间)。

        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this) {
            @Override
            protected int getExtraLayoutSpace(RecyclerView.State state) {
                return 300;
            }
        };

下面是实际效果:

在重写getExtraLayoutSpace()之前

RecyclerView stuttering

重写之后:

RecyclerView stuttering fixed

Feed 卡片上的按钮

下面就让我们开始卡片上那些按钮(目前只有喜欢和评论按钮)的工作吧.从视频中的效果来看是非常赞的,圆形扩散的selector

Ripple effect

Android Lollipop中的Ripple效果

按钮所使用的Selector无非就是棒棒糖中介绍的Ripple效果-一种水波扩散效果。已经有无数关于Ripple效果的文章,这里就不赘述了,我只给出两篇文章的链接:

我们项目中实现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:

<?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效果。

Send comment button

注:评论发布按钮对应的类是SendCommentButton.java,这一节中讲解的内容都是在这个类中。结合代码更容易看明白。

关于这个按钮SendCommentButton,我将用到下面几个android元素:

ViewAnimator:作为SendCommentButton的基类,它有一个对我们非常有用的特性,可以设置子view切换时的进入和退出效果。如果你对ViewAnimator很陌生,其直接子类ViewFlipper, ViewSwitcher或间接子类ImageSwitcher, TextSwitcher应该很熟悉。

自定义view:包括xml以及inflate xml的代码

标签消除冗余的view

实现

好了,现在来开始实现,从动画开始。实际上我们需要四个动画,发送状态两个(进入和退出),完成状态两个(进入和退出,不过是相反的方向):

发送状态:

滑出顶端

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="100"
    android:fromYDelta="0"
    android:interpolator="@android:anim/linear_interpolator"
    android:toYDelta="-80%p" />

从顶端滑入

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="100"
    android:fromYDelta="-80%p"
    android:interpolator="@android:anim/linear_interpolator"
    android:toYDelta="0" />

完成状态:

从底部滑入

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="100"
    android:fromYDelta="80%p"
    android:interpolator="@android:anim/linear_interpolator"
    android:toYDelta="0" />

滑出底部

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="100"
    android:fromYDelta="0"
    android:interpolator="@android:anim/linear_interpolator"
    android:toYDelta="80%p" />

现在来实现button的布局:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:id="@+id/tvSend"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="SEND"
        android:textColor="#ffffff"
        android:textSize="12sp" />
    <TextView
        android:id="@+id/tvDone"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="✓"
        android:textColor="#ffffff"
        android:textSize="12sp" />
</merge>

因为ViewAnimator本身是一个FrameLayout,因此需要使用标签来减少了一层view。

最后,我们只有一个需求了,当button切换到完成状态之后,隔两秒它会自动切换回去。

代码很简单:

public class SendCommentButton extends ViewAnimator implements View.OnClickListener {
    public static final int STATE_SEND = 0;
    public static final int STATE_DONE = 1;
    private static final long RESET_STATE_DELAY_MILLIS = 2000;
    private int currentState;
    private OnSendClickListener onSendClickListener;
    private Runnable revertStateRunnable = new Runnable() {
        @Override
        public void run() {
            setCurrentState(STATE_SEND);
        }
    };
    public SendCommentButton(Context context) {
        super(context);
        init();
    }
    public SendCommentButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.view_send_comment_button, this, true);
    }
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        currentState = STATE_SEND;
        super.setOnClickListener(this);
    }
    @Override
    protected void onDetachedFromWindow() {
        removeCallbacks(revertStateRunnable);
        super.onDetachedFromWindow();
    }
    public void setCurrentState(int state) {
        if (state == currentState) {
            return;
        }
        currentState = state;
        if (state == STATE_DONE) {
            setEnabled(false);
            postDelayed(revertStateRunnable, RESET_STATE_DELAY_MILLIS);
            setInAnimation(getContext(), R.anim.slide_in_done);
            setOutAnimation(getContext(), R.anim.slide_out_send);
        } else if (state == STATE_SEND) {
            setEnabled(true);
            setInAnimation(getContext(), R.anim.slide_in_send);
            setOutAnimation(getContext(), R.anim.slide_out_done);
        }
        showNext();
    }
    @Override
    public void onClick(View v) {
        if (onSendClickListener != null) {
            onSendClickListener.onSendClickListener(this);
        }
    }
    public void setOnSendClickListener(OnSendClickListener onSendClickListener) {
        this.onSendClickListener = onSendClickListener;
    }
    @Override
    public void setOnClickListener(OnClickListener l) {
        //Do nothing, you have you own onClickListener implementation (OnSendClickListener)
    }
    public interface OnSendClickListener {
        public void onSendClickListener(View v);
    }
}

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:

<?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:

<?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

错误振动提示

最后,我们将增加一个视频中没有出现的效果-当发送评论时如果输入框中没有内容,则播放振动动画。下面是效果图:

Error shaking

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插值器来重复播放这个动画,只有几行代码:

<?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:

<?xml version="1.0" encoding="utf-8"?>
<cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:cycles="2"/>

cycle_2.xml hosted with ❤ by GitHub

通过如下的代码来将这个动画应用到按钮中:

btnSendComment.startAnimation(AnimationUtils.loadAnimation(this, R.anim.shake_error));

这就是今天所讲的全部内容了,这里是含有SendCommentButton类的最后一次提交:last commit 。下篇文章中我们将继续实现概念视频中的效果。

源码

讨论中例子的源码在这里:repository.

作者: Miroslaw Stanek

英文原文:InstaMaterial concept (part 3) - Feed and comments buttons 

转载请注明出处:http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0209/2451.html

 

来自:InstaMaterial概念设计