Android:复杂滚动布局的一种适配思路

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/DrkCore/article/details/51120570

【转载请注明出处】
笔者:DrkCore
博客:http://blog.csdn.net/drkcore/article/details/51120570

App 的首页向来是个寸土寸金的地方,如何在首页吸引用户是 PM 的工作,而对于我们开发者而言要做的就是适配。想必大家一定遇到过类似如下的设计稿:

这里写图片描述

在设计稿上我们可以看到一些重点:

  • 页面可以上下滚动
  • 元素比较繁多
  • 不同页之间有分割线或者间隙

基于上面的几点我们的思路无非分为两条,第一条思路是:

使用ScrollView包裹LinearLayout并动态add多个布局

这种方法的是可以解决问题的,各个板块可以写成子布局,之间的风格线和间隙可以通过添加黑色的高度为 1 像素的 View 和设置 MarginTop 来解决,重复的元素使用动态修改高度的 ListView 来适配。

确实简单粗暴,但缺点是无法复用 View 比较浪费内存。

第二条思路,也就是本篇博文将要论述的思路:

将包括顶部的轮播图、中间的活动版面甚至是分割线和间隙在内的东西都当成 Item 布局,并使用单个ListView适配

较之前者,该方法不但解决了复用 View 的问题而且视图的层次更少。

使用 RecyclerView 也是可以的不过思路其实和 ListView 一样,这里不再赘述。具体实现同样也在给出的源码中,请自行查阅。

之后的章节笔者会详细地阐述实现和代码,篇幅略长。如果你想直接看源代码的话可以直接跳转到文末。

BaseAdapter 的初步封装

考虑到原有的 BaseAdapter 功能太弱,在正式开始工作之前有必要对之进行初步的封装。首先我们在 BaseAdapter 中实现类似 RecyclerView 的 ViewHolder 机制,这里先定义一个 AbsViewHolder 的基类,如下:

public static abstract class AbsViewHolder<Item> {

    // 这里使用一个泛型来引用该ViewHolder对应的数据项
    private Item item;
    private int position;
    private final View view;

    // 这里省略get和set方法,完整的代码参见文末GitHub
}

之后我们定义 BaseAdapter 的基类代码如下:

// 每个Adapter都要确定自己能适配的数据类型和使用的ViewHolder
public abstract class CoreAdapter<Item, Holder extends AbsViewHolder<Item>> extends BaseAdapter {

    // 将数据源封装进Adapter中方便后续的开发
    private final List<Item> data = new ArrayList<>();

    /* 继承 */

    // inflater的单例
    private LayoutInflater inflater;

    @Override
    public final int getCount () {
        // 由于数据源在内部并且是final的,这里可以写死
        return data.size();
    }

    @Override
    public final Item getItem (int position) {
        // 同上,写死
        return data.get(position);
    }

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

    @Override
    public final View getView (int position, View convertView, ViewGroup parent) {
        // 获取inflater的单例
        if (inflater == null) {
            //当getView方法被调用时必定已经被set进ListView了
            //此时parent.getContext()不会空指针
            inflater = LayoutInflater.from(parent.getContext());
        }

        //获取position对应的数据和viewType
        int viewType = getItemViewType(position);
        Item data = getItem(position);

        //实现ViewHolder机制
        Holder holder;
        if (convertView == null) {
            holder = createViewHolder(inflater, parent, viewType);
            convertView = holder.getView();
            convertView.setTag(holder);// 绑定ViewHolder
        } else {
            holder = (Holder) convertView.getTag();
        }

        // 将数据和position绑定到ViewHolder上
        holder.setPosition(position);
        holder.setItem(data);
        bindViewData(holder, position, data, viewType);

        return convertView;
    }

    /* 回调 */

    //创建ViewHolder实例
    //如果你使用过RecyclerView的Adapter的话这里你会发现这里的思想与之基本一致
    protected abstract Holder createViewHolder (LayoutInflater inflater, ViewGroup parent, int viewType);

    //绑定数据到ViewHolder实例上
    //也是RecyclerView.Adapter的思想
    protected abstract void bindViewData (Holder holder, int position, Item data, int viewType);

    /* 数据展示 */

    //从外部添加数据项到Adapter内部的数据源,并刷新视图
    public final void display (Item... items) {
        this.data.clear();
        Collections.addAll(this.data, items);
        notifyDataSetChanged();
    }

    public final void display (Collection<? extends Item> items) {
        this.data.clear();
        this.data.addAll(items);
        notifyDataSetChanged();
    }

    //后续还可以拓展上add,remove之类的方法,这里略去。
    //完整代码请参阅GitHub
}

经过以上的修改我们已经将 ViewHolder 机制封装进了 Adapter 之中,为后续的开发剩下了大量的代码。

具体实现

由于我们将所有的控件都当成Item布局不用想也知道这里会有数不清的 if-else 要写,这显然也是不够优雅的。要解决这个问题我们就需要用到策略模式和策略工厂。

我们首先将每个 Item 的操作逻辑封装成对应的策略,一个策略处理一种 Item 的实例化、绑定数据和事件回调等逻辑。如下:

/*单个Item布局的策略*/
public abstract class AbsItemOperator<Item, Holder extends AbsViewHolder<Item>> {

    //该策略支持的数据类型的单例
    private Type type;

    //判断该策略是否能够适配obj的实例
    public final boolean canHandleObject (Object obj) {
        if (type == null) {
            //获取实现类的第一个泛型参数作为该策略的可适配类型
            //ClassUtil请参见GitHub源代码
            Type types[] = ClassUtil.getGenericParametersType(getClass());
            type = types[0];
        }
        if (obj instanceof Collection) {
            //出于安全和可维护性考虑这里禁止直接适配Collection的子类。
            throw new IllegalStateException("不允许直接适配Collection或者其子类");
        }
        return type == obj.getClass();
    }

    /*回调*/

    //创建ViewHolder
    public abstract Holder createViewHolder (LayoutInflater inflater, ViewGroup parent);

    //绑定数据到ViewHolder
    public abstract void bindViewData (Holder holder, Item data);

}

我们可以在这个基类中看到两个抽象方法:createViewHolder()bindViewData(),只要在这两个方法里面实现对 Item 的处理即可。

之后我们需要将 Adapter 封装一个策略工厂,负责策略的匹配和调度。代码如下:

public final class FlexibleAdapter extends CoreAdapter<Object, AbsViewHolder<Object>> {

    //使用被final修饰的List来保存所有支持的布局策略
    private final List<AbsItemOperator> operators = new ArrayList<>();

    //在构造的时候必须指明所有支持的策略
    public FlexibleAdapter (AbsItemOperator<?, ?>... operators) {
        Collections.addAll(this.operators, operators);
    }

    /* 继承 */

    @Override
    protected AbsViewHolder<Object> createViewHolder (LayoutInflater inflater, ViewGroup parent, int viewType) {
        //createViewHolder方法是在之前的CoreAdapter之中定义的
        //其中viewType参数来自getItemViewType(),其值也是目标策略在策略列表operators中的序数
        //取出策略,直接调度
        return (AbsViewHolder<Object>) operators.get(viewType).createViewHolder(inflater, parent);
    }

    @Override
    protected void bindViewData (CoreAdapter.AbsViewHolder<Object> viewHolder, int position, Object data, int viewType) {
        //同上,根据viewType取出策略并调度
        operators.get(viewType).bindViewData(viewHolder, data);
    }

    @Override
    public int getItemViewType (int position) {
        //获取position对应的数据实例
        //并通过AbsItemOperator.canHandleObject()方法逐一匹配合适的策略
        Object obj = getItem(position);
        for (int i = 0, count = operators.size(); i < count; i++) {
            if (operators.get(i).canHandleObject(obj)) {
                return i;// 可处理
            }
        }
        // 没有策略能够适配该数据
        throw new IllegalStateException("指定数据类型不存在可用的operator");
    }

    @Override
    public int getViewTypeCount () {
        //策略列表的大小就是布局类型数
        return operators.size();
    }

}

至此整体的框架就已经搭建完毕,往后如果有新增的Item布局只需要新增一个独立的策略实现类即可,十分灵活。

实现一个策略

这里我们写一个策略以加深对这种思路的理解。布局文件很简单:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="@color/core_white">

    <View
        android:layout_width="24dp"
        android:layout_height="16dp"
        android:layout_gravity="center_vertical"
        android:background="@color/core_holo_green_light"/>

    <TextView
        android:id="@+id/textView_listView_tip"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"/>
</FrameLayout>

有点经验的开发者应该立马就能想象到它实例化后的样子,主体只是一个用来显示文字的 TextView 而已。接下来是它对应的策略:

public class TipOperator extends AbsItemOperator<Tip, TipViewHolder> implements View.OnClickListener {

    //写一个AbsViewHolder的基类,ViewHolder本身其实还可以深挖不少东西,这里篇幅有限就不再赘述
    public static class TipViewHolder extends AbsViewHolder<Tip> {

        public TipViewHolder(View v) {
            super(v);
        }
    }

    /*继承*/

    @Override
    public TipViewHolder createViewHolder(LayoutInflater inflater, ViewGroup parent) {
        //实例化布局
        View view = inflater.inflate(R.layout.activity_listview_tip, parent, false);
        //绑定事件
        view.setOnClickListener(this);
        return new TipViewHolder(textView);
    }

    @Override
    public void bindViewData(TipViewHolder holder, Tip data) {
        //绑定数据
        TextView textView = (TextView) holder.getView().findViewById(R.id.textView_listView_tip);
        textView.setText(data.tip);
    }

    /*回调*/

    @Override
    public void onClick(View v) {
        //因为CoreAdapter中已经实现了ViewHolder机制因而itemView.getTag返回的必定是对应的ViewHolder,在这里就是TipViewHolder
        //在设计上ViewHolder和ItemView的实例是绑定在一起的,只是携带者的position和data一直在变化
        //因此只要获得ViewHolder就能获得当前的position和data
        TipViewHolder holder = (TipViewHolder) v.getTag();
        if(onTipClickListener != null){
            //回调监听者
            onTipClickListener.onTipClick(v,holder.getPosition(),holder.getItem());
        }
    }

    /*接口*/

    //监听者接口
    public interface OnTipClickListener{

        void onTipClick(View v,int position,Tip tip);

    }

    private OnTipClickListener onTipClickListener;

    public TipOperator setOnTipClickListener(OnTipClickListener onTipClickListener) {
        this.onTipClickListener = onTipClickListener;
        return this;
    }
}

实际使用时如下:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ListView listView = new ListView(this);
    listView.setDivider(null);
    listView.setDividerHeight(0);
    setContentView(listView);
    setTitle(getClass().getSimpleName());

    //实例话FlexibleAdapter并填入所有的布局策略
    FlexibleAdapter adapter = new FlexibleAdapter(new RotateOperator(), new TipOperator(), new PanelOperator(), new MsgOperator(), new DividerOperator(), new SpanOperator());
    listView.setAdapter(adapter);

    //逐一添加每个数据项
    List<Object> list = new ArrayList<>();
    Rotate rotate = new Rotate(R.layout.activity_listview_rotate_1, R.layout.activity_listview_rotate_2, R.layout.activity_listview_rotate_3);
    list.add(rotate);
    list.add(new Divider());

    list.add(new Span());

    int times = 5;
    while (times-- > 0) {
        list.add(new Divider());
        Tip tip = new Tip("这是一个Tip");
        list.add(tip);
        list.add(new Divider());
        Panel panel = new Panel("这个是内容,我就随便输入一些什么东西");
        list.add(panel);
        list.add(new Divider());
        list.add(new Span());
    }

    list.add(new Divider());
    times = 20;
    while (times-- > 0) {
        list.add(new Msg("这是文本35435435453545354"));
    }

    //刷新到界面上
    adapter.display(list);
}

以上代码运行起来后效果如下:

之后新增一个布局只需要新增一个对应的策略即可。

最后附上源码地址:

http://download.csdn.net/detail/drkcore/9505492

猜你喜欢

转载自blog.csdn.net/DrkCore/article/details/51120570