Android开发 | 布局与控件(二)

简析ListView与RecycleView布局

前文中提及的几种布局均为基本布局,而实际应用中最为广泛使用的布局还是本文的ListView与RecycleView。本文就上述两种布局进行介绍及简单总结。

一、ListView

ListView相当常见,比如QQ的好友列表:

/androiddev-charpter3-ui-2/qq.webp
QQ好友列表ListView

以及微信的列表:

/androiddev-charpter3-ui-2/wechat.webp
微信好友列表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;
    }
}

LayoutInflaterinflate()方法接收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

给作者倒杯卡布奇诺 ~
Albresky 支付宝支付宝
Albresky 微信微信