简析ListView与RecycleView布局
前文中提及的几种布局均为基本布局,而实际应用中最为广泛使用的布局还是本文的ListView与RecycleView。本文就上述两种布局进行介绍及简单总结。
一、ListView
ListView相当常见,比如QQ的好友列表:
QQ好友列表ListView
以及微信的列表:
微信好友列表ListView
均是用ListView作为布局框架。
1.1 基本用法
首先是创建ListView的基本布局,ListView作为父控件时,再可以创建子控件并对子控件添加逻辑支持(后面讲)。
ListView基本布局:
1
2
3
4
<ListView
android:id= "@+id/listViewTest"
android:layout_width= "match_parent"
android:layout_height= "match_parent" />
然后在活动中利用适配器(Adapter),将适配器对象传递给布局,就能完成基本的ListView与数据间的关联。此处实例使用的是最基本的字符串数组,然后利用ArrayAdapter将泛型指定为String,实例化adapter对象。
ArrayAdapter的构造函数的参数列表依次表示:当前的上下文context、ListView布局子选项id、适配的数据。其中,android.R.layout.simple_list_item_1为安卓内置布局,其仅能用于显示简单的文本。
实例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class testTemp extends AppCompatActivity {
private String [] names = {
"Ada" , "Beth" , "Cathy" , "Dee" , "Ella" , "Fiona" , "Gina" , "Hannah" , "Ida" , "Jenny" , "Kitty" , "Linda" , "Molly" , "Nancy" , "Olivia" , "Penny" , "Queen" , "Rose" , "Sally" , "Tina" , "Uma" , "Vicky" , "Wendy" , "Xena" , "Yvonne" , "Zoe"
};
@Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . activity_main );
ArrayAdapter < String > adapter = new ArrayAdapter <> ( this , android . R . layout . simple_list_item_1 , names );
ListView listView = findViewById ( R . id . listViewTest );
listView . setAdapter ( adapter );
}
}
1.2 自定义ListView布局
首先为ListView项目定义实体类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.cosyspark.listview ;
public class Names {
private String name ;
private int headerImgId ;
public Names ( String name , int headerImgId ) {
this . name = name ;
this . headerImgId = headerImgId ;
}
public String getName () {
return name ;
}
public int getHeaderImgId () {
return headerImgId ;
}
}
对于ListView的项,定义其布局为图片(头像)和文本框的组合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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= "wrap_content" >
<ImageView
android:id= "@+id/headerImg"
android:layout_width= "40dp"
android:layout_height= "60dp"
android:layout_marginLeft= "10dp" />
<TextView
android:id= "@+id/sbName"
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
android:layout_gravity= "center_vertical"
android:layout_marginLeft= "10dp" />
</LinearLayout>
1.3 自定义ArrayAdatper
前面ListView的项内容是通过将ArrayAdapter的泛型指定为String传递给ListView的,为使其能够根据自定义的Names类进行适应,需要重写getView()方法,这里我们通过自定义一个NamesAdatper类,使其继承自ArrayAdapter,再对子方法getView()进行重写:
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
public class NameAdapter extends ArrayAdapter < Names > {
private int resourceId ;
public NameAdapter ( Context context , int textViewResourceId , List < Names > objects ) {
super ( context , textViewResourceId , objects );
resourceId = textViewResourceId ;
}
/* 自定义内部类,用于对控件实例进行缓存
// class ViewHolder {
// ImageView headerImage;
// TextView nameText;
// }
*/
@Override
public View getView ( int position , View convertView , ViewGroup parent ) {
Names thisName = getItem ( position ); //获取当前项的Name实例
View view ;
// *1* ViewHolder viewHolder;
// if (convertView == null) {
view = LayoutInflater . from ( getContext ()). inflate ( resourceId , parent , false );
// *2* viewHolder = new ViewHolder();
// viewHolder.headerImage = (ImageView) view.findViewById(R.id.headerImg);
// viewHolder.nameText = (TextView) view.findViewById(R.id.sbName);
// view.setTag(viewHolder);
// } else {
view = convertView ;
// viewHolder = (ViewHolder) view.getTag();
// }
//// *3* ImageView headerImage = view.findViewById(R.id.headerImg);
//// TextView nameText = view.findViewById(R.id.sbName);
//// headerImage.setImageResource(thisName.getHeaderImgId());
//// nameText.setText(thisName.getName());
// viewHolder.headerImage.setImageResource(thisName.getHeaderImgId());
// viewHolder.nameText.setText(thisName.getName());
return view ;
}
}
LayoutInflater
的inflate()
方法接收3个参数,分别为:当前的上下文context
、父布局
、是否为该View添加父布局
。关于这里的inflate()
方法,详见:
Android筑基——深入理解 LayoutInflater.inflate() 方法
上述代码中注释掉的部分为优化方案。由于原本的代码每次在调用getView()方法时(当ListView的项被快速滚动时),view总会被重新加载一遍(1 ),大大降低了效率,这会使其成为性能瓶颈。此时,利用判断未使用到的参数convertView(用于缓存之前加载的布局)是否为空,可以提高运行效率与性能。
除此之外,原本的getView()方法中,函数体最后每次都调用了findViewById()方法,这也是降低性能的表现(其实无需每次都获取控件实例的id)。这里借助ViewHolder即能进行优化(控件实例被缓存在ViewHolder之中,规避每次重新获取控件id),如注释部分所示。
而此时的活动类应按如下方法定义:
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
public class MainActivity extends AppCompatActivity {
private List < Names > namesList = new ArrayList <> ();
@Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . activity_main );
initNames ();
NameAdapter adapter = new NameAdapter ( this , R . layout . name_item , namesList );
ListView listView = findViewById ( R . id . listViewTest );
listView . setAdapter ( adapter );
// add click listener to listview
listView . setOnItemClickListener ( new AdapterView . OnItemClickListener () {
@Override
public void onItemClick ( AdapterView <?> adapterView , View view , int i , long l ) {
Names name = namesList . get ( i );
Toast . makeText ( MainActivity . this , "You clicked " + name . getName (), Toast . LENGTH_SHORT ). show ();
}
});
}
private void initNames () {
for ( int i = 0 ; i < 1 ; i ++ ) {
Names aName = new Names ( "Adam" , R . drawable . upstream );
namesList . add ( aName );
Names aName1 = new Names ( "Bob" , R . drawable . upstream1 );
namesList . add ( aName1 );
Names aName2 = new Names ( "Wanda" , R . drawable . upstream2 );
namesList . add ( aName2 );
Names aName3 = new Names ( "Zomba" , R . drawable . upstream3 );
namesList . add ( aName3 );
Names aName4 = new Names ( "Rocky" , R . drawable . upstream4 );
namesList . add ( aName4 );
Names aName5 = new Names ( "Frank" , R . drawable . upstream5 );
namesList . add ( aName5 );
Names aName6 = new Names ( "Jennifer" , R . drawable . upstream6 );
namesList . add ( aName6 );
Names aName7 = new Names ( "Dom" , R . drawable . upstream7 );
namesList . add ( aName7 );
Names aName8 = new Names ( "Blade" , R . drawable . upstream8 );
namesList . add ( aName8 );
Names aName9 = new Names ( "Chandler" , R . drawable . upstream9 );
namesList . add ( aName9 );
Names aName10 = new Names ( "Eadie" , R . drawable . upstream10 );
namesList . add ( aName10 );
Names aName11 = new Names ( "Granny" , R . drawable . upstream11 );
namesList . add ( aName11 );
Names aName12 = new Names ( "Henry" , R . drawable . upstream12 );
namesList . add ( aName12 );
Names aName13 = new Names ( "Lena" , R . drawable . upstream13 );
namesList . add ( aName13 );
Names aName14 = new Names ( "Morgan" , R . drawable . upstream14 );
namesList . add ( aName14 );
Names aName15 = new Names ( "Nigger" , R . drawable . upstream15 );
namesList . add ( aName15 );
Names aName16 = new Names ( "Obi-wan" , R . drawable . upstream16 );
namesList . add ( aName16 );
Names aName17 = new Names ( "Phoenix" , R . drawable . upstream17 );
namesList . add ( aName17 );
}
}
}
其中,initNames()
方法用作初始化ListView
项内容。
1.4 ListView响应点击事件
ListView内置onItemClick()回调方法,故该点击事件监听与往期的普通控件定义方法相仿:
1
2
3
4
5
6
7
listView . setOnItemClickListener ( new AdapterView . OnItemClickListener () {
@Override
public void onItemClick ( AdapterView <?> adapterView , View view , int i , long l ) {
Names name = namesList . get ( i );
Toast . makeText ( MainActivity . this , "You clicked " + name . getName (), Toast . LENGTH_SHORT ). show ();
}
);
二、RecycleView
ListView的最大特点是它只能纵向布局,即用户只能上下滑动进行操纵。而RecycleView则更为灵活,它既能纵向布局,也能水平布局,且点击事件定义更为灵活,可以满足更复杂的响应事件(自定义),这些特点(优点)都是ListView所不具有的。
笔者参照的书籍是郭霖老师的《Android第一行代码》第2版,该版本基于Android 7.0和Java语言进行讲述,而如今安卓版本已经更迭到12,Android的依赖库也经历不少改进。书中这部分内容伊始对RecycleView是作为外部依赖库引入,即andorid.support.v*.xxx(通过在项目gradle中添加compile ‘android.support.v*.xxx’),而如今RecycleView已经作为androidX库的内置部分了,故在gradle中直接implementation ‘andoridx.recycleview:xxx’即可。
2.1 基本用法
RecycleView基本布局文件定义与ListView类似,不过需要注意:需要引入完整的包,尽管其包含在androidX依赖库中,但仍然不是系统SDK的内容(从需要在gradle中引入可以看出),具体的基本定义如下:
1
2
3
4
5
6
7
8
9
<LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "match_parent" >
<androidx.recyclerview.widget.RecyclerView
android:id= "@+id/recycler_view"
android:layout_width= "match_parent"
android:layout_height= "match_parent" />
</LinearLayout>
至于RecycleView的项布局定义,直接使用ListView的项布局定义layout_name_item.xml即可。
2.2 自定义NameAdapter适配器
将NameAdapter继承自RecycleView.Adapter,泛型指定为NameAdapter.ViewHolder,这里的ViewHolder为我们在NameAdapter中自定义的内部类。
内部类的构造函数ViewHolder(View itemView)缓存控件,NamesAdapter构造函数的传递数据源。
由于NamesAdapter继承自RecyclerView.Adapter,那么必须重写onCreateViewHolder()、onBindViewHolder()以及getItemCount()三个方法。
onCreateViewHolder()用于加载R.layout.name_item布局,同时添加了点击事件的监听。这里与ListView不同的是,RecycleView不存在内置的点击回调函数,需要我们自己构造,这也是RecycleView灵活之处的体现。如果认为这么一来造成了不便,那就是想多了,因为ListView内置的点击监听方法不能响应项中复杂化的控件(比如项中布局嵌套,多层控件),而RecycleView的点击监听方法需我们自己构造,就没有这些约束。
onBindViewHolder()在项目滚动出来时加载,用于对项目中的数据进行赋值。
getItemCount()用于计算项的数目。
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
public class NamesAdapter extends RecyclerView . Adapter < NamesAdapter . ViewHolder > {
private List < Names > myNameList ;
static class ViewHolder extends RecyclerView . ViewHolder {
ImageView headerImage ;
TextView nameText ;
// listener based on View
View nameView ;
public ViewHolder ( View itemView ) {
super ( itemView );
nameView = itemView ;
headerImage = ( ImageView ) itemView . findViewById ( R . id . headerImg );
nameText = ( TextView ) itemView . findViewById ( R . id . sbName );
}
}
public NamesAdapter ( List < Names > nameList ) {
myNameList = nameList ;
}
@Override
public ViewHolder onCreateViewHolder ( ViewGroup parent , int viewType ) {
View view = LayoutInflater . from ( parent . getContext ()). inflate ( R . layout . name_item , parent , false );
final ViewHolder holder = new ViewHolder ( view );
holder . nameView . setOnClickListener ( new View . OnClickListener () {
@Override
public void onClick ( View view ) {
int position = holder . getBindingAdapterPosition ();
Names name = myNameList . get ( position );
// do something
Toast . makeText ( view . getContext (), "You clicked view " + name . getName (), Toast . LENGTH_SHORT ). show ();
}
});
holder . headerImage . setOnClickListener ( new View . OnClickListener () {
@Override
public void onClick ( View view ) {
int position = holder . getBindingAdapterPosition ();
Names name = myNameList . get ( position );
// do something
Toast . makeText ( view . getContext (), "You clicked " + name . getName () + "'s header image" , Toast . LENGTH_SHORT ). show ();
}
});
return holder ;
}
@Override
public void onBindViewHolder ( ViewHolder holder , int position ) {
Names thisName = myNameList . get ( position );
holder . headerImage . setImageResource ( thisName . getHeaderImgId ());
holder . nameText . setText ( thisName . getName ());
}
@Override
public int getItemCount () {
return myNameList . size ();
}
}
2.3 活动类的定义
活动类在onCreate()方法中先初始化项列表,然后加载项(笔者将这一部分另外封装到函数loadDefaultLayout()中了)。loadDefaultLayout时,先创建一个NameAdapter实例,然后获取RecycleView布局控件,再创建一个线性布局的管理器(这里是举例,也可以是其他类的管理器,见后文),设置线性布局的位置分布(水平布局,可略过),最后为布局控件设置布局管理,代码参考如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . activity_main );
initNames ();
loadDefaultLayout ();
}
private void loadDefaultLayout () {
NamesAdapter adapter = new NamesAdapter ( namesList );
RecyclerView recyclerView = findViewById ( R . id . recycler_view );
LinearLayoutManager layoutManager = new LinearLayoutManager ( this );
layoutManager . setOrientation ( LinearLayoutManager . HORIZONTAL );
recyclerView . setLayoutManager ( layoutManager );
recyclerView . setAdapter ( adapter );
}
2.4 RecycleView横向瀑布样式
上文将加载项的代码独立出来就是为了实现这里的多样式切换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public boolean onOptionsItemSelected ( @NonNull MenuItem item ) {
RecyclerView recyclerView = findViewById ( R . id . recycler_view );
NamesAdapter adapter = new NamesAdapter ( namesList );
switch ( item . getItemId ()) {
case R . id . staggeredGridLayout :
// 横向瀑布样式
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager ( 3 , StaggeredGridLayoutManager . VERTICAL );
recyclerView . setLayoutManager ( staggeredGridLayoutManager );
recyclerView . setAdapter ( adapter );
// 横向瀑布样式
break ;
case R . id . gridLayout :
// ···
break ;
case R . id . restore :
loadDefaultLayout ();
default :
break ;
}
return true ;
}
2.5 RecycleView瀑布流样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public boolean onOptionsItemSelected ( @NonNull MenuItem item ) {
RecyclerView recyclerView = findViewById ( R . id . recycler_view );
NamesAdapter adapter = new NamesAdapter ( namesList );
switch ( item . getItemId ()) {
case R . id . staggeredGridLayout :
// ···
case R . id . gridLayout :
// RecycleView瀑布流样式
GridLayoutManager gridLayoutManager = new GridLayoutManager ( this , 4 );
recyclerView . setLayoutManager ( gridLayoutManager );
recyclerView . setAdapter ( adapter );
// RecycleView瀑布流样式
break ;
case R . id . restore :
loadDefaultLayout ();
default :
break ;
}
return true ;
}
上述两节的onOptionsItemSelected()方法为笔者切换布局的菜单的重写方法,无需在意,核心是代码片段中注释区域的三行,显而易见,这就仅是将上文的布局管理器更换了而已,同时传入了不同的新参数,参数“3” 表示3列(故布局定义文件中须将控件的宽度属性设置为wrap_content或者常量,如果设置为match_parent则会导致只显示1列)。
三、ListView与RecycleView的demo