Adapter

单布局

ListView/GridView + Adapter可以说是Android程序员最常用的组件了. 我们应该尽量简化Adapter的代码以提高开发效率. 首先来看看不做任何封装的Adapter需要哪些步骤:

  1. 继承自BaseAdpter

  2. 维护一个List数据并实现4个抽象方法

  3. 声明一个ViewHolder

  4. 在getView中判断convertView是否为空, 为空则inflate一个布局, 初始化ViewHolder, 初始化控件, 并将ViewHolder通过setTag设置到convertView中.

  5. 如果不为空则通过getTag将ViewHolder取出来

  6. 为控件设置值

可以看到需要做的事情还是很多的. utils包中有一个ViewHolder类, 此类可以代替自己声明的ViewHolder, 并且不用写将ViewHolder与convertView绑定的代码. 控件可以直接通过如下方式声明:

TextView textView = ViewHolder.get(convertView, R.id.textView);

ViewHolder中使用了SparseArray来缓存视图, 内部也是用过给convertView设置tag来达到缓存的目的. 并且通过泛型来避免类型转换. 整体逻辑很简单:

public class ViewHolder {
    @SuppressWarnings("unchecked")
    public static <T extends View> T get(View view, int id) {
        SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArray<>();
            view.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = view.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
}

下面的内容详细可以参考 打造万能的ListView GridView 适配器

虽然有了ViewHolder能减少很多代码量, 但是这不是极限, 还可以继续减少. 比如内部维护的List数据, 另外三个抽象方法等都是可以不用写的. 这里我们引用GitHub上的一个开源工具 base adapter helper. 现在来看看如何使用base adapter helper:

public class HomeAdapter extends QuickAdapter<Attraction> {

    public HomeAdapter(Context context, int layoutResId) {
        super(context, layoutResId);
    }

    @Override
    protected void convert(BaseAdapterHelper helper, Attraction item) {
        helper.setText(R.id.tv_name, item.name)
                .setText(R.id.tv_des, item.profile)
                .setText(R.id.tv_distance,
                        String.format(context.getResources().getString(R.string.item_home_KM),
                                Attraction.formatDistance(item.distance)))
    }
}

可以调用如下方法修改Adapter的数据

homeAdapter.add(data);
homeAdapter.addAll(list);
homeAdapter.removeAll();
homeAdapter.remove(0);
homeAdapter.set(0, data);
...

base adapter helper的原理在此处就不赘述了, 参考 Android base-adapter-helper 源码分析与扩展. 使用base adapter helper可以减少大量重复的代码, 也可以通过扩展BaseAdapterHelper以适应自己的编程习惯.

接下来看看base adapter helper的getItemId方法的实现:

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

很多时候, getItemId的返回值都是position. 如果我们需要拿到实体类的id, 就必须这么写:

adapter.getItem(position).id;

通常情况下, ListView中的某些控件是需要与网络API交互的, 而基本上网络API需要的只是一条数据的id. 因此我改造了getItemId方法, 在某些情况下, 此方法能直接返回实体类中的id.

/**
 * 配合{@link QuickAdapter}使用, 传入{@link QuickAdapter}中的数据类型如果实现此接口,
 * 则可以直接调用{@link QuickAdapter#getItemId(int)}来获取数据的id.
 */
public interface IdObject {
    long getId();
}
@Override
public long getItemId(int position) {
    T data = this.data.get(position);
    return data instanceof IdObject ? ((IdObject) data).getId() : position;
}

上述的某些情况就是指T类型实现了IdObject接口. 以上就是对Android适配器的简化及优化. 但是base adapter helper使用虽然简单, 也还是有不足的地方. 比如不支持多布局, 只支持BaseAdapter, 不支持其他类型比如ViewPager的Adapter等.

以上就是单布局的基本原理与使用方法. 单布局是APP中最常用的, 下面介绍不是很常见的多布局的封装.

多布局

多布局是指一个适配器中使用一个以上的布局文件. 主要是依赖于覆写BaseAdapter中的getViewTypeCountgetItemViewType方法. 多布局使用了代理, 将convert方法代理给多个代理类去实现, 有兴趣的可以看这里, 这里只讲使用方法. 直接上代码:

final QuickMultiAdapter<Integer> adapter = new QuickMultiAdapter<>(this);
adapter.addItemViewDelegate(new LeftDelegate());
adapter.addItemViewDelegate(new RightDelegate());

listView.setAdapter(adapter);
adapter.replaceAll(getData());
    class LeftDelegate implements ItemViewDelegate<Integer> {

        @Override public int getItemViewLayoutId() {
            return R.layout.i_text;
        }

        @Override public boolean isForViewType(Integer item, int position) {
            return item % 2 == 0;
        }

        @Override public void convert(BaseAdapterHelper helper, Integer item, int position) {
            helper.setText(R.id.text, item + "");
        }
    }
    class RightDelegate implements ItemViewDelegate<Integer> {

        @Override public int getItemViewLayoutId() {
            return R.layout.i_text_right;
        }

        @Override public boolean isForViewType(Integer item, int position) {
            return item % 2 != 0;
        }

        @Override public void convert(BaseAdapterHelper helper, Integer item, int position) {
            helper.setText(R.id.text, item + "");
        }
    }

效果:

两个布局的差别就是一个TextView的gravity是left, 同时有margin值, 另一个的gravity为right, 没有margin值. 如果是双数则显示gravity是left的TextView, 否则显示gravity是right的TextView. 代码也比较好理解, 接下来看看Delegate接口的声明以及每个方法的含义:

/**
 * Adapter中多布局代理
 * @param <T> 数据源类型
 * @param <H> ViewHolder类型
 */
public interface BaseItemViewDelegate<T, H extends BaseAdapterHelper> {

    /** 布局资源id **/
    int getItemViewLayoutId();

    /** 判断该position是否要加载此类型的布局 **/
    boolean isForViewType(T item, int position);

    /**
     * 当需要条目将被展示到界面上时, 通过此方法适配界面
     * @param helper ViewHolder
     * @param item 数据
     * @param position 位置
     */
    void convert(H helper, T item, int position);
}

ItemViewDelegate继承了BaseItemViewDelegate, 同时声明H为BaseAdapterHelper, 所以在不需要自定义AdapterHelper的情况下, 建议直接使用ItemViewDelegate:

/**
 * ViewHolder类型为BaseAdapterHelper的快捷代理接口
 */
public interface ItemViewDelegate<T> extends BaseItemViewDelegate<T, BaseAdapterHelper> {}

RecyclerView

作为目前超级灵活超级NB的杀手级控件 - RecyclerView, 怎么能少了它呢? 直接上代码:

final RecyclerAdapter<Integer> adapter = new RecyclerAdapter<Integer>(this, R.layout.i_text) {
    @Override protected void convert(BaseAdapterHelper helper, Integer item, int position) {
        helper.setText(R.id.text, item + "");
    }
};

使用方法与ListView/GridView一样, 只不过继承从QuickAdapter变成RecyclerAdapter. 多布局也一样, 区别只是从QuickMultiAdapter换成RecyclerMultiAdapter. 下面看一个多布局的例子:

首先是效果:

示例中有两种布局, 一种是日期, 一种是课程详情. 日期占满一行, 详情占半行. XML以及实体省略:

RecyclerMultiAdapter<Playback> adapter = new RecyclerMultiAdapter<>(getActivity());
adapter.addItemViewDelegate(new DateDelegate());
adapter.addItemViewDelegate(new ContentDelegate());

GridLayoutManager manager = new GridLayoutManager(getActivity(), 2);
manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override public int getSpanSize(int position) {
        return adapter.getItem(position).isDate ? 2 : 1;
    }
});

recycler.setLayoutManager(manager);
recycler.addItemDecoration(new PlaybackItemDecoration());
recycler.setAdapter(adapter);
    class DateDelegate implements ItemViewDelegate<Playback> {

        @Override public int getItemViewLayoutId() {
            return R.layout.i_playback_date;
        }

        @Override public boolean isForViewType(Playback item, int position) {
            return item.isDate;
        }

        @Override public void convert(BaseAdapterHelper helper, Playback item, int position) {
            helper.setText(R.id.tv_date, item.formattedDate);
        }
    }

    class ContentDelegate implements ItemViewDelegate<Playback> {

        @Override public int getItemViewLayoutId() {
            return R.layout.i_playback;
        }

        @Override public boolean isForViewType(Playback item, int position) {
            return !item.isDate;
        }

        @Override public void convert(BaseAdapterHelper helper, Playback item, int position) {
            helper.setText(R.id.tv_class_index, item.getClassNumberHint())
                    .setText(R.id.tv_teacher, "教师: " + item.teacher)
                    .setText(R.id.tv_time, "时间: " + item.getFormattedTimePeriod())
                    .setTag(R.id.parent, position)
                    .setOnClickListener(R.id.parent, listener);
        }
    }
    class PlaybackItemDecoration extends RecyclerView.ItemDecoration {
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            int position = parent.getChildAdapterPosition(view);
            Playback playback = adapter.getItem(position);
            if (playback.isDate) {
                outRect.set(0, divider, 0, divider);
                if (position == 0) outRect.top = 0;
            } else {
                outRect.set(divider, divider, divider, divider);
                if (playback.subPosition % 2 == 0)
                    outRect.left = divider * 2;
                else
                    outRect.right = divider * 2;
            }
        }
    }

除了Adapter的代码外, 其他均是RecyclerView的基本用法, 如使用SpanSizeLookup来设置每个条目占用的单元数, 使用ItemDecoration来控制条目之间的间隔等.

Last updated