自定义View:2、组合视图

昨天联系了自定义View的第一类,直接继承View自己画图。其实很多情况下,我们并不需要自己去从头开始画图,而是将现有的各种视图组合在一起,方便我们的使用。那这种情况下该怎么做呢? 我们先来看看实现的效果怎么样(下图中,我将组合视图与CardView和RecyclerView结合在一起,看起来效果还蛮不错的): device-2014-12-09-110725 其实操作起来也很简单,与直接继承View的办法类似,一共3个步骤:

  1. 编写Layout,可以在代码中设置Layout,也可以直接通过XML配置Layout,然后在代码中直接解析就可以。
  2. 编写属性
  3. 组合在一起成为一个新的View
  4. 使用我们新的编写的View

很简单吧?我们依次来看,具体应该怎么来做。

一:编写对应的Layout

代码如下:

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
29
30
31
32
33
34
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout\_width="match\_parent"
android:layout\_height="match\_parent"
>
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:scaleType="centerInside"
android:src="@drawable/ic_launcher"
android:id="@+id/avator"/>
<TextView
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_marginLeft="@dimen/activity\_horizontal_margin"
android:paddingTop="@dimen/activity\_vertical\_margin"
android:layout_toRightOf="@id/avator"
android:layout_alignTop="@id/avator"
android:id="@+id/name"
android:text="name"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<TextView
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_marginTop="@dimen/activity\_vertical_margin"
android:layout_alignLeft="@id/name"
android:layout_below="@id/name"
android:text="Description here"
android:layout_alignBottom="@id/avator"
android:id="@+id/description"/>

</RelativeLayout>

视图的定义就不需要多说了,比较简单,其界面效果就是其中的名片小卡片,包括了一张图像,名字和简单的简介。

二、编写属性

为了以后能够更加方便的使用,我们当然最好为我们新编写的视图增加属性设置,其实和上一篇文章当中介绍的基本上一样,贴上代码。

1
2
3
4
5
<declare-styleable name="NameCard">
<attr name="avatar" format="reference"/>
<attr name="name" format="string"/>
<attr name="description" format="string"/>
</declare-styleable>

我们为其制定了3个属性,分别是图像,姓名,介绍。通过这些,我们就可以在XML中直接指定默认的属性。

三、构建新的View

思路: 第一步:我们需要加载我们在步骤1中编写的Layout 第二步:根据XML属性或者用户的设置更改其中图像,姓名和介绍的内容。 其他的,就交给RelativeLayout去做吧。 代码如下:

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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class NameCard extends RelativeLayout{

public static class NameCardContent {
private Drawable mAvatarImageDrawable;
private String mName;
private String mDescription;

public NameCardContent() {
}

public NameCardContent(Drawable avatarImageDrawable, String name, String description) {
mAvatarImageDrawable = avatarImageDrawable;
mName = name;
mDescription = description;
}

public Drawable getAvatarImageDrawable() {
return mAvatarImageDrawable;
}

public void setAvatarImageDrawable(Drawable avatarImageDrawable) {
mAvatarImageDrawable = avatarImageDrawable;
}

public String getName() {
return mName;
}

public void setName(String name) {
mName = name;
}

public String getDescription() {
return mDescription;
}

public void setDescription(String description) {
mDescription = description;
}
}

private NameCardContent mNameCardContent = new NameCardContent();

private ImageView mImageViewAvator;
private TextView mTextViewName;
private TextView mTextViewDescription;

public NameCard(Context context) {
this(context, null);
}

public NameCard(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public NameCard(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);

TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NameCard, 0, 0);
try {
mNameCardContent.mAvatarImageDrawable = typedArray.getDrawable(R.styleable.NameCard_avatar);
mNameCardContent.mName = typedArray.getString(R.styleable.NameCard_name);
mNameCardContent.mDescription = typedArray.getString(R.styleable.NameCard_description);
} finally {
typedArray.recycle();;
}

inflate(context, R.layout.namecard, this);

mImageViewAvator = (ImageView) findViewById(R.id.avator);
mTextViewName = (TextView) findViewById(R.id.name);
mTextViewDescription = (TextView) findViewById(R.id.description);

mImageViewAvator.setImageDrawable(mNameCardContent.getAvatarImageDrawable());
mTextViewName.setText(mNameCardContent.getName());
mTextViewDescription.setText(mNameCardContent.getDescription());
}

public Drawable getAvatarImageDrawable() {
return mNameCardContent.getAvatarImageDrawable();
}

public void setAvatarImageDrawable(Drawable avatarImageDrawable) {
mNameCardContent.setAvatarImageDrawable(avatarImageDrawable);
mImageViewAvator.setImageDrawable(avatarImageDrawable);
}

public String getName() {
return mNameCardContent.getName();
}

public void setName(String name) {
mNameCardContent.setName(name);
mTextViewName.setText(name);
}

public String getDescription() {
return mNameCardContent.getDescription();
}

public void setDescription(String description) {
mNameCardContent.setDescription(description);
mTextViewDescription.setText(description);
}
}

四、使用

根据我们上面的所想要的效果,首先,为CardView编写子视图界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout\_width="match\_parent"
android:layout\_height="wrap\_content"
>
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout\_width="match\_parent"
android:layout\_height="wrap\_content"
android:id="@+id/cardview"
android:layout_gravity="center"
card_view:cardCornerRadius="10dp"
card_view:cardElevation="10dp"
>
<me.happyhls.androiddemo.view.NameCard
android:layout\_width="match\_parent"
android:layout\_height="wrap\_content"
android:id="@+id/namecard">
</me.happyhls.androiddemo.view.NameCard>
</android.support.v7.widget.CardView>
</LinearLayout>

然后编写Activity对应的RecyclerView的界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout\_width="match\_parent"
android:layout\_height="match\_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity\_vertical\_margin"
android:paddingLeft="@dimen/activity\_horizontal\_margin"
android:paddingRight="@dimen/activity\_horizontal\_margin"
android:paddingTop="@dimen/activity\_vertical\_margin">
<android.support.v7.widget.RecyclerView
android:layout\_width="match\_parent"
android:layout\_height="match\_parent"
android:id="@+id/recycleview"
android:layout_gravity="center"
></android.support.v7.widget.RecyclerView>
</LinearLayout>

最后,在Activity中初始化RecyclerView和数据,代码如下:

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
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
public class TestNameCard extends Activity {

private static final String TAG = TestNameCard.class.getSimpleName();

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.lollipop_recyclerviewandcardview);

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycleview);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);

List<NameCard.NameCardContent> items = new ArrayList<NameCard.NameCardContent>();
Drawable drawable = getResources().getDrawable(R.drawable.ic_launcher);
for (int i = 0; i < 100; i++) {
items.add(new NameCard.NameCardContent(drawable, "Name" + i, "Description" + i));
}
MyAdapter adapter = new MyAdapter(this, items);
recyclerView.setAdapter(adapter);
}

static class MyAdapter extends RecyclerView.Adapter<ViewHolder> {

private List<NameCard.NameCardContent> mItems;
private LayoutInflater mLayoutInflater;

public MyAdapter(Context context, List<NameCard.NameCardContent> items) {
this.mItems = new ArrayList<NameCard.NameCardContent>(items);
mLayoutInflater = LayoutInflater.from(context);
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
Log.d(TAG, "Adapter creating view for " + position);
View view = mLayoutInflater.inflate(R.layout.namecard_item, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
viewHolder.mNameCard = (NameCard) view.findViewById(R.id.namecard);
return viewHolder;
}

@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Log.d(TAG, "Adapter binding view for " + position);
viewHolder.mNameCard.setAvatarImageDrawable(mItems.get(position).getAvatarImageDrawable());
viewHolder.mNameCard.setName(mItems.get(position).getName());
viewHolder.mNameCard.setDescription(mItems.get(position).getDescription());
}

@Override
public int getItemCount() {
return mItems.size();
}
}

static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View view) {
super(view);
}

NameCard mNameCard;
}
}

思考:优化的空间? 需要注意的是,上面我们的实现中,仅仅是展示了组合视图的相关的原理和RecyclerView/CardView的使用,但实际的生产环境中不会这么简单,几个简单的点: 1、NameCard当中,我们为每一个名片都保存了Drawable,而且是强引用,同时我们观察代码,可以发现所有的DrawableActivity中的List中的NameCardContent里面,在上面的代码中,我们所有的Drawable都是指向同一个对象,因此不会占用太多的内容空间,但在实际应用当中,不同的人对应的头像必然是不同的,那这个时候就不能再这样使用了,否则会必然导致OOM。(解决办法,加入Cache,保存Drawable对应的地址或者Id) 2、关于视图层次,上面的代码中,NameCard是一个RelativeLayout,但我们注意到其中加载的namecard.xml仍然其中任然有一层RelativeLayout,其实是不需要的,多于的,由于这个视图会多次被解析,因此这样必然会严重影响加载速度,所以此处应该将namecard.xml最外层去掉RelativeLayout,设置为merge标签即可。