Android 初级面试者拾遗(前台界面篇)之 ListView 和 RecyclerView

includecmath

ListView 和 RecyclerView

最常用和最难用的控件

由于手机屏幕空间有限,无法显示全部内容。当有大量数据需要展示的时候,借助列表控件。通过手指上下滑动,使得屏幕内外的数据不断进出。

最基本的列表工作模式需要列表控件数据源,列表控件能够进行交互和展示数据。但是列表控件不与数据源直接打交道,Adapter 接口充当桥梁,关联数据源与列表控件,增强可扩展性,适配不同数据类型数据源。例如:ArrayAdapter 数组、CursorAdapter 游标。

数据源可能来自:

  • 静态数据
  • 网络数据
  • 数据库

ListView

ListView extends AdapterView extends ViewGroup.

  • Adapter 管理数据源
  • AdapterView 展示数据并处理交互

数据无法直接传递给 ListView,需要借助 setAdapter() 适配器来完成。例如 ArrayAdapter<> 泛型指定要适配的数据类型。

ListView listView = (ListView) findViewById(R.id.listview);
listView.setAdapter(adapter);

自定义适配器

适配数据源并重写一组父类方法:

构造函数:例如 ArrayAdapter 依次传入当前上下文、ListView 子项布局 id、数据源。

ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects)

getView() 方法:用于每个子项(单行)进入屏幕可视区域时候调用,根据数据源绘制子项布局。

程序示例:

public class MySimpleArrayAdapter extends ArrayAdapter<String> {
    private final Context context;
    private final String[] values;

    public MySimpleArrayAdapter(Context context, String[] values) {
        super(context, R.layout.rowlayout, values);
        this.context = context;
        this.values = values;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        LayoutInflater inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View rowView = inflater.inflate(R.layout.rowlayout, parent, false);
        TextView textView = (TextView) rowView.findViewById(R.id.label);
        ImageView imageView = (ImageView) rowView.findViewById(R.id.icon);
        textView.setText(values[position]);
        // Change the icon for Windows and iPhone
        String s = values[position];
        if (s.startsWith("Windows7") || s.startsWith("iPhone")
                || s.startsWith("Solaris")) {
            imageView.setImageResource(R.drawable.no);
        } else {
            imageView.setImageResource(R.drawable.ok);
        }

        return rowView;
    }
}

其他常用方法:

getCount() 方法

返回适配器表示的数据源中一共有多少项数据。

notifyDataSetChanged() 方法

数据源的数据发生变化,通知 ListView 更新数据重新绘制视图。

提升 ListView 运行效率

避免在 Adapter 的 getView() 方法中重新加载布局(子项布局)

public abstract View getView(int position, View convertView, ViewGroup parent)

convertView 用于将加载好的布局进行缓存,根据 convertView 是否为空,判断能否重用布局,减少 LayoutInflater.inflate() 调用次数从而提升性能。

减少 findViewById() 方法获取控件实例的调用次数

通过内部类 ViewHolder 对控件实例进行缓存,调用 View 的 setTag() 方法,将 ViewHolder 对象存储在 View 中。

程序示例:

public class MyPerformanceArrayAdapter extends ArrayAdapter<String> {
    private final Activity context;
    private final String[] names;

    static class ViewHolder {
        public TextView text;
        public ImageView image;
    }

    public MyPerformanceArrayAdapter(Activity context, String[] names) {
        super(context, R.layout.rowlayout, names);
        this.context = context;
        this.names = names;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View rowView = convertView;
        // reuse views
        if (rowView == null) {
            LayoutInflater inflater = context.getLayoutInflater();
            rowView = inflater.inflate(R.layout.rowlayout, null);
            // configure view holder
            ViewHolder viewHolder = new ViewHolder();
            viewHolder.text = (TextView) rowView.findViewById(R.id.TextView01);
            viewHolder.image = (ImageView) rowView
                    .findViewById(R.id.ImageView01);
            rowView.setTag(viewHolder);
        }

        // fill data
        ViewHolder holder = (ViewHolder) rowView.getTag();
        String s = names[position];
        holder.text.setText(s);
        if (s.startsWith("Windows7") || s.startsWith("iPhone")
                || s.startsWith("Solaris")) {
            holder.image.setImageResource(R.drawable.no);
        } else {
            holder.image.setImageResource(R.drawable.ok);
        }

        return rowView;
    }
}

存在多种类型的子项布局的场景

基本实现方式:

  • 定义视图类型常量
  • 重写 getViewTypeCount() 方法和 getItemViewType(int position) 方法
  • 重写 getView() 方法

getViewTypeCount() 方法

返回一共有多少个不同的视图类型(布局),这些视图将由 getView() 方法创建。

getItemViewType(int position) 方法

根据子项所处的位置判断具体类型并返回。

程序示例:

@Override
public int getViewTypeCount() {
  return 2;
}

@Override
public int getItemViewType(int position) {
  return (contactList.get(position).getContactType() == ContactType.CONTACT_WITH_IMAGE) ? 0 : 1;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
  View v = convertView;
  int type = getItemViewType(position);
  if (v == null) {
    // Inflate the layout according to the view type
   LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   if (type == 0) {
     // Inflate the layout with image
     v = inflater.inflate(R.layout.image_contact_layout, parent, false);
  }
  else {
    v = inflater.inflate(R.layout.simple_contact_layout, parent, false);
  }
 }
 // fill data
 Contact c = contactList.get(position);

 TextView surname = (TextView) v.findViewById(R.id.surname);
 TextView name = (TextView) v.findViewById(R.id.name);
 TextView email = (TextView) v.findViewById(R.id.email);

 if (type == 0) {
   ImageView img = (ImageView) v.findViewById(R.id.img);
   img.setImageResource(c.imageId);
 }

 surname.setText(c.surname);
 name.setText(c.name);
 email.setText(c.email);

 return v;
}

android_listview_multiple_layout

ListView 的 RecycleBin 机制

ListView 即使加载成百上千条数据,依然不会发生 OOM 的原因——RecycleBin 机制。

RecycleBin

RecycleBin 类中存在两个重要的数组:

  • mActiveViews 屏幕上可见的 View
  • mScrapViews 屏幕外不可见的 View

当 ListView 子项 View 进入屏幕可视区域时候,从 RecycleBin 的 mScrapViews 获取 View 作为 convertView 参数传递给 Adapter 的 getView() 方法。

ListView 有如 View 一般执行视图绘制流程 onMeasure()onLayout()onDraw()。在 onLayout() 方法中会调用一个关键方法 layoutChildren(),该方法由 ListView 具体实现进行子元素的布局,同时完成 ListView 对子项 View 的添加和删除操作。

layoutChildren() 方法主要逻辑:

  1. 若 Adapter 中的数据集发生变化,则将 ListView 中的所有子项 View 放到 RecycleBin 中的 mScrapViews 废弃 View 集合。
    若 Adapter 中的数据集无变化,则将 ListView 中的所有子项 View 放到 RecycleBin 中的 mActiveViews 激活 View 集合。
  2. 调用 detachAllViewsFromParent() 方法解除子项 View 与 ListView 之间的关联。
  3. 重新将子项 View 添加到 ListView 中。根据 mLayoutMode 判断如何进行添加,fillDown() 方法将子 View 从指定的 position 自上而下填充 ListView,fillUp() 则相反自下而上进行填充。

RecyclerView

自定义适配器

适配器继承自 RecyclerView.Adapter<>,并将泛型指定为内部类 Adapter.ViewHolder。

重写一组父类方法:

  • onCreateViewHolder()
    加载子项布局(LayoutInflater inflate()),创建 ViewHolder 实例。
  • onBindViewHolder()
    用于每个子项(单行)进入屏幕可视区域时候调用,根据数据源位置绘制子项布局。
  • getItemCount()
    返回数据源的长度

程序示例:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    private String[] mDataset;

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder
    public static class MyViewHolder extends RecyclerView.ViewHolder {
        // each data item is just a string in this case
        public TextView mTextView;
        public MyViewHolder(TextView v) {
            super(v);
            mTextView = v;
        }
    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public MyAdapter(String[] myDataset) {
        mDataset = myDataset;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        // create a new view
        TextView v = (TextView) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.my_text_view, parent, false);
        ...
        MyViewHolder vh = new MyViewHolder(v);
        return vh;
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        holder.mTextView.setText(mDataset[position]);

    }

    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return mDataset.length;
    }
}

RecyclerView vs ListView

固有的 ViewHolder 模式规范

RecyclerView.Adapter 默认采用 ViewHolder 模式,减少 findViewById() 方法获取控件实例的调用次数。

使用 LayoutManager 支持多种布局方式

RecyclerView 借助 LayoutManager 能够灵活地将列表控件放入不同的容器(LinearLayout, GridLayout)。

ListView 布局只能实现纵向排列,而 RecyclerView 将排列工作 setLayoutManager() 交给 LayoutManager 布局排列接口,因此可以定制出不同排列方式(横向、瀑布流布局)。

通知 Adapter 的数据变化更加灵活

不仅 notifyDataSetChange() 方法,RecyclerView 可以使用 notifyItemRangeChanged() 等方法实现局部更新数据并重绘视图。

子项视图的动画效果更容易实现

  • RecyclerView.ItemAnimator
  • RecyclerView.ItemDecoration
阅读 2.7k

从 Android 到全栈
Java 程序设计语言查漏补缺 Android 应用开发基础知识巩固 Flutter 跨平台方案研究
16 声望
4 粉丝
0 条评论
16 声望
4 粉丝
文章目录
宣传栏