Android内部分享[6]——列表和适配器详解

列表复用

列表的一个重大职责是复用 View, 因为我们的可见区域是有限的,要不断的回收再利用。我画了一张列表在手机上展示的关系示意图:

列表展示重用示意图

如上图,手机屏幕是呈现给用户的窗口,这个窗口是一个固定宽高的区域(上图绿色区域),而一个列表是可以无限长度的(分页加载),我们不需要创建这么多的子 View ,这样极大的浪费内存。所以这里我们可以利用适配器来完成列表(ListView)的列表项(item)的复用。在屏幕滚动的同时,如上面箭头所示意那样我们可以将看过的 View 拿来继续复用,这样可以保证列表项是无限的,而我们创建的 View 是有限的几个即可。

ListView 和 GrideView

在 Android 5.0 之前 ListView 和 GrideView 是列表布局的重要控件,而在 5.0 之后推出了号称更快的 RecyclerView, 这是我们后面研究的重点。

适配器是一个连接数据和AdapterView的桥梁,通过它能有效地实现数据与 AdapterView 的分离设置,使 AdapterView 与数据的绑定更加简便,修改更加方便。将数据源的数据适配到 ListView 中的常用适配器有:ArrayAdapter、SimpleAdapter 和 SimpleCursorAdapter, 实际工作中,常用自定义适配器。即继承于BaseAdapter的自定义适配器类。

首先定义布局和实体类:

<ListView
    android:id="@+id/main_button_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

如果你要显示成网格,只需要修改这里为 <GridView> 即可,下面其他步骤全部一致。

public class MainMenu {

    public String menuName;
    public Class<? extends BaseActivity> turnToClass;

    public MainMenu(String menuName, Class<? extends BaseActivity> turnToClass) {
        this.menuName = menuName;
        this.turnToClass = turnToClass;
    }
}

继承自 BaseAdapter 定义适配器内容:

public class MainMenuAdapter extends BaseAdapter {

    private List<MainMenu> mMenusData;

    public MainMenuAdapter(List<MainMenu> menus){
        mMenusData = menus;
    }
    @Override
    public int getCount() {
        if(mMenusData == null) return 0;
        return mMenusData.size();
    }

    @Override
    public MainMenu getItem(int position) {
        return mMenusData.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if(convertView == null){
            viewHolder = new ViewHolder();
            convertView = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.item_main_button, null, true);
            viewHolder.turnButton = convertView.findViewById(R.id.btn_item);
            convertView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) convertView.getTag();
        }
        MainMenu menu = getItem(position);
        viewHolder.turnButton.setText(menu.menuName);
        return convertView;
    }

    static class ViewHolder{
        Button turnButton;
    }
}

注意上面的 getView() 中的内容,是实现 item 重用的关键。

然后创建按钮的集合并绑定到适配器,也就是说将数据绑定到适配器并创建适配器实例。

private List<MainMenu> mButtonsData = new ArrayList<>();
private void initButtonsData(){
    mButtonsData.add(new MainMenu("Thread 创建线程", TestThreadCreateActivity.class));
    mButtonsData.add(new MainMenu("Runnable 创建线程", TestRunnableCreateActivity.class));
    mButtonsData.add(new MainMenu("runOnUiThread 切换到 UI 线程", TestRunOnUiThreadActivity.class));
    mButtonsData.add(new MainMenu("View.post(Runnable) 切换到 UI 线程", TestViewPostActivity.class));
    mButtonsData.add(new MainMenu("postDelayed 切换到 UI 线程", TestPostDelayedActivity.class));
    mButtonsData.add(new MainMenu("AsyncTask 使用", TestAsyncTaskActivity.class));
}
mAdapter = new MainMenuAdapter(mButtonsData);
mListView.setAdapter(mAdapter);

列表通过 setAdapter() 方法和适配器进行关联。接下来我们再给列表的每一个 item 绑定一个点击事件:

mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        turnTo(mAdapter.getItem(position).turnToClass);
    }
});

这里要注意的一点是,因为我们的 item 里面有 <Button> 所以这里的 ItemClick 事件不会回调,需要给 <Button> 添加如下属性来取消它自身的可点击性:

android:focusable="false"
android:clickable="false"
android:focusableInTouchMode="false"

示例运行结果

RecyclerView

据 Google 的文档上说 RecyclerView 做了很多优化的地方,所以推荐使用 RecyclerView 来实现诸如 列表,网格,瀑布流,不规则等布局样式。而且 RecyclerView 有一个很大的特点就是可以实现局部刷新。

首先要添加支持库:

dependencies {
    implementation 'com.android.support:recyclerview-v7:28.0.0'
}

在布局中添加 RecyclerView:

<?xml version="1.0" encoding="utf-8"?>
<!-- A RecyclerView with some commonly used attributes -->
<android.support.v7.widget.RecyclerView
    android:id="@+id/my_recycler_view"
    android:scrollbars="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

创建 RecyclerView 适配器: RecyclerView 适配器需要去重写(扩展)RecyclerView.Adapter 类。

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private String[] mDataset;
    
    // 注意这个必须继承自 RecyclerView.ViewHolder
    public static class MyViewHolder extends RecyclerView.ViewHolder {

        public TextView textView;
        public MyViewHolder(TextView v) {
            super(v);
            textView = v;
        }
    }

    // 构造函数,传入数据
    public MyAdapter(String[] myDataset) {
        mDataset = myDataset;
    }


    //创建布局内容并返回 ViewHolder 
    @Override
    public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        // 创建一个 item 内容
        TextView v = (TextView) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.my_text_view, parent, false);
        ...
        MyViewHolder vh = new MyViewHolder(v);

        //这里可以根据 viewType 来返回不同的 ViewHolder, 例如:
        /*switch(viewType){
            case VIEW_TYPE1:
                return vh;
            case VIEW_TYPE2:
                return vh2;
        }*/
        return vh;
    }

    // 在这里更新视图内容,比如设置文本,图片等
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        holder.textView.setText(mDataset[position]);

    }

    // 返回数据大小
    @Override
    public int getItemCount() {
        return mDataset.length;
    }
}

和 ListView 不同的是 RecyclerView 的 onCreateViewHolder() 方法中的 ViewHolder 对象必须是继承自 RecyclerView.ViewHolder 的实例,在这个方法中我们可以根据不同的视图要求,创建不同的(多个) ViewHolder 根据 viewType 来返回。

设置 RecyclerView 的布局方式,然后将适配器绑定到 RecyclerView 对象上:

recyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);

// 这个设置是如果你知道你的所有 item 布局是一致的则设置为 true 可以提高性能
recyclerView.setHasFixedSize(true);

// 这里使用线性布局方式显示
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);

// 创建适配器并绑定到 RecyclerView
mAdapter = new MyAdapter(myDataset);
recyclerView.setAdapter(mAdapter);

当然我们也可以设置列表间隔,而且很灵活很方便,如果我们的数据发生变化,可以调用 notifyItemChanged() 方法来更新界面显示。

除了线性布局 LinearLayoutManager, 还有 GridLayoutManager(网格) , StaggeredGridLayoutManager(瀑布流)。