MaterialList源码解析
项目地址:MaterialList,分析的版本:v3.2.2,Demo 地址:MaterialList Demo
本文结构
- 1、功能介绍
- 2、总体设计
- 3、详细设计
- 4、MaterialList自定义布局
- 5、总结
1. 功能介绍
1.1 简介
MaterialList是一个帮助Android开发者获取漂亮CardView的Android库,通过这个库你可以很容易实现具有Material Design风格的ListView,MaterialList中内置了7种类型的CardView,
1.2 如何使用
首先,在你的layout中声明一个MaterialListView:
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
接着,绑定MaterialListView到一个变量,并设置Item动画
1 | mListView = (MaterialListView) findViewById(R.id.material_listview); |
然后设置dismiss监听和ItemTouchListener
1 | // Set the dismiss listener |
最后添加Card
1 | mListView.getAdapter().addAtStart(new Card.Builder(this) |
通过给Provider设置不同的layout,从而获取不同的CardView.
通过设置ListCardProvider和R.layout.material_list_card_layout,可以实现带listView的CardView.
2、总体设计
2.1 MaterialList总体由四个部分组成
- Crad作为整个工程的基本部件,不过Card内容的组装是由Card.Builder和CardProvider(CardProvider实现Observable接口)共同完成,ItemView内View的实例化和给View添加Action都由CardProvider完成。
- CardLayout 继承自LinearLayout,作为ItemView它被MaterialListAdapter.ViewHolder包裹起来,同时实现Observer接口,监听CardProvider的数据变化。
- MaterialListAdapter继承自RecyclerView.Adapter,通过关联MaterialListView.OnAdapterItemsChanged实现添加ItemView和删除ItemView的回调,通过关联MaterialListView.OnSwipeAnimation实现删除ItemView的动画播放效果,同时实现Observer接口,监听CardProvider的数据变化。
- MaterialListView继承自RecyclerView,通过关联RecyclerItemClickListener,实现ItemView的单击和长按回调,通过关联SwipeDismissRecyclerViewTouchListener实现滑动删除ItemView的回调。
3、详细设计
3.1 类关系图
以上是 MaterialList的主要类的关系图,跟总体设计中介绍的一样大致分为四部分。3.2 核心功能介绍
3.2.1 Card是如何完成内容组装成为ItemView的
1 | final CardProvider provider = new Card.Builder(this) |
我们通过观察上面的代码,首先我们通过new Card.Builder(this)获取Builder实例,接着设置setDismissible(),将属性设置为mDismissible,使它可以移除。
1 |
|
然后我们通过withProvider(new CardProvider())注入CardProvider实例,此时将获得CardProvider实例,我们通过这个实例设置ItemView的color,layout,title,Action,最后通过provider.endConfig().build()我们就能获取实例Card,整个组合过程就是标准的Builder模式,不过Card就像一个空壳,真正持有ItemView核心属性的是CardProvider。
接着我们回到withProvider()和setLayout()这两个方法,我们知道获取不同风格的CardView就是通过设置不同的layout和CardProvider来实现,但这个布局视图控件是怎样的添加和初始化的呢?
首先我们看MaterialListAdapter的内部类ViewHolder,ViewHolder在类中关联了CardLayout。
1 | public static class ViewHolder extends RecyclerView.ViewHolder { |
接着MaterialListAdapter继承RecyclerView.Adapter并重写它的两个方法getItemViewType() onBindViewHolder(),分别绑定LayoutId和绑定holder,并对cardLayout进行初始化。
1 |
|
我们看到onBindViewHolder()中调用了holder.build(),holder.build()内又调用了CardLayout的build(),build()内代码如下:
1 | public void build(@NonNull final Card card) { |
我们可以看到,实际上build()是调用了CardProvider的render()方法来对Card视图布局进行初始化,render()的核心代码如下:
1 | public void render(@NonNull final View view, @NonNull final Card card) { |
上面代码中render()主要完成layout中控件的初始化,和初始化Action,并调用Action的抽象方法onRender(),而Action的子类TextAction,实现了onRender(),如下:
1 |
|
TextAction在onRender()中获取textview,并设置了单击事件监听,在onClick()中回调OnActionClickListener的onActionClicked()方法,这样使Layout上View只要设置了Action就能添加单击事件回调。通过上面的流程CardLayout将作为MaterialListView的ItemView,并布局到界面上。
3.2.2 如何实现MaterialList上ItemView的滑动移除,
MaterialList可以通过左右滑动移除ItemView,这个功能是很实用的,这个效果是如何实现呢?
首先我们将MaterialListView作为切入口,MaterialListView继承RecyclerView,它通过setOnTouchListener(mDismissListener)设置了TouchListener,这个mDismissListener就是SwipeDismissRecyclerViewTouchListener,它继承View.OnTouchListener类,并重写了onTouch(),以下是onTouch()的核心代码:
1 |
|
我们知道一个完整的滑动事件,是先ACTION_DOWN,接着ACTION_MOVE,最后才ACTION_UP,我们将从这三方面分析onTouch(),
- 首先在ACTION_DOWN中我们获取触控点坐标,接着我们遍历mRecyclerView的childView,计算出这个触控点落在哪一个childView上,从而获取downView和position
- 然后在ACTION_MOVE中我们通过判断触控点在x轴方向滑动的距离大于最小有效滑动距离和x轴方向滑动的距离的二分之一大于Y轴方向滑动的距离,则设置mSwiping = true,并对事件进行拦截,同时播放属性动画,使mDownView沿滑动x轴方向移动,并设置alpha
- 最后在ACTION_UP中通过mVelocityTracker.getXVelocity()获取触控点1s内在x轴方向的速度absVelocityX,如果absVelocityX大于等于最小速度值和小于或等于最大速度值,absVelocityX大于y轴方向速度,且mSwiping为true,这些条件都满足的话,则设置dismiss = true;或是如果x轴方向滑动的距离大于二分之一downView宽时,且mSwiping==true,则设置dismiss = true。接下来我们设置dismiss动画,并在动画结束时,调用performDismiss()。而我们downView移除的时候下面的view会慢慢的往上面顶,填补这个空白,这个是如何做到的呢?核心就是performDismiss()内设置的属性动画,其核心代码如下:
1 | private void performDismiss(final View dismissView, final int dismissPosition) { |
我们从上面的代码可以看出通过ValueAnimator来逐渐将downView的高度从originalHeight到1,实现下面的view慢慢往上顶的动画效果。在动画结束时,会调用onDismiss()来将downView从adapter中移除,同时发送一个 cancel event,重置参数和释放资源。
3.2.3 观察者模式在MaterialList中的应用
从3.1的类关系图我们可以知道MaterialListAdapter和CardLayout都实现了Observer接口,CardProvider继承Observable类,CardProvider注册观察者过程如下,首先在MaterialListAdapter 中的onBindViewHolder(),代码如下:
1 |
|
holder会调用build(),方法内又会调用CardLayout的build(),我们看下面CardLayout的build()的方法代码
1 | public void build(@NonNull final Card card) { |
从上面代码可以看出CardProvider注册了CardLayout观察者,而在MaterialListAdapter中的add(),同样给CardProvider注册了MaterialListAdapter观察者。代码如下:
1 | public void add(final int position, @NonNull final Card card, final boolean scroll) { |
这样每当我们CardProvider调用setTitle()等设置属性的方法时,就会调用notifyObservers()时,代码如下:
1 | public T setTitle(@NonNull final String title) { |
这样会触发CardLayout和MaterialListAdapter的回调函数update()1
2
3
4
5
6
7
8@Override
//CardLayout重写的update()
public void update(final Observable observable, final Object data) {
if(data == null) {
build(mCard);//初始化card,接下来调用render()来初始化LayoutId内的控件
((CardProvider) observable).notifyDataSetChanged(getCard());
}
}
1 |
|
通过这个观察者模式,我们可以很轻松地在修改CardProvider的Title,TitleColor,Drawable后,实时的在MaterialListView显示出刚刚的修改。
4、 MaterialList自定义布局
MaterialList库有7个layout 的xml文件,已经足够我们使用了,不过我们可不可以设置自己的XML文件进去呢?回答肯定是可以的,MaterialList就有很好的扩展性,通过本文的第三部分,我们知道了Card的布局控件初始化是在CardProvider的render()完成的,单击事件的添加是通过Action,这样我们要实现imageView的单击事件,就需要通过继承TextViewAction来事件,详细文件如下:
layout_new.xml1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78<?xml version="1.0" encoding="utf-8"?>
<com.dexafree.materialList.card.CardLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/MainLayout">
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardView"
style="@style/Material_Card_View"
card_view:cardCornerRadius="@dimen/card_corner_radius"
card_view:cardElevation="@dimen/card_elevation">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@null"/>
<TextView
android:id="@+id/title"
style="@style/Material_Card_Title"
android:layout_gravity="bottom"
tools:text="Title"/>
</FrameLayout>
<TextView
style="@style/Material_Card_Subtitle_24dp"
android:id="@+id/supportingText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="@dimen/big_padding"
android:textColor="@color/description_color"
android:textSize="@dimen/description_size"
tools:text="Test description"/>
<include layout="@layout/divider"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/share_text_button"
style="@style/Material_Action"
android:clickable="true"
tools:text="Action 1"
android:src="@drawable/share_text_button"/>
<ImageView
android:id="@+id/mark_text_button"
style="@style/Material_Action"
tools:text="Action 2"
android:clickable="true"
android:src="@drawable/mark_text_button"/>
<ImageView
android:id="@+id/star_text_button"
style="@style/Material_Action"
android:clickable="true"
tools:text="Action 3"
android:src="@drawable/star_text_button"/>
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</com.dexafree.materialList.card.CardLayout>
ImageViewAction继承于TextViewAction,使添加的ImageView可以添加监听点击事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class ImageViewAction extends TextViewAction{
private OnActionClickListener mListener;
public ImageViewAction(@NonNull Context context) {
super(context);
}
protected void onRender(@NonNull final View view, @NonNull final Card card) {
ImageView imageView = (ImageView) view;
imageView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if(mListener != null) {
mListener.onActionClicked(view, card);
}
}
});
}
}
这样我们就可以构建新的Card1
2
3
4
5
6
7
8
9
10
11final Card card =new Card.Builder(this)
.setTag("IMAGE_BUTTONS_CARD")
.setDismissible()
.withProvider(new CardProvider())
.setLayout(R.layout.layout_new)
.setTitle("Hi there")
.setDescription("I've been added on top!")
.addAction(R.id.share_text_button, new ImageViewAction(this))
.addAction(R.id.star_text_button, new ImageViewAction(this))
.addAction(R.id.mark_text_button, new ImageViewAction(this))
.setDrawable(R.drawable.photo).endConfig().build();
效果图如下:
5、总结
到此为止,整个MaterialList基本分析完了,从介绍使用到深入分析源码设计,最后介绍如何扩展,这个过程就像一次次历险,每一次发现都有不一样的收获,今后还会继续写这类型的博客,继续read the fucking source code!!!