Android——RecyclerView入门学习之RecyclerView.Adapter(三)

学习资料:

之前使用RecyclerView.Adapter,基本就类似套用公式,死步骤,对Adapter感到既熟悉又陌生。从去年我开始接触学习Android之时,RecyclerView已经开始大量被运用,逐步取代ListView。遂,正好,那就先直接学习RecyclerView.Adapter相关知识

1. RecyclerView.Adapter适配器

RecyclerView.Adapter,一个抽象类,并支持泛型

public static abstract class Adapter<VH extends ViewHolder> {
   ...
}

定义一个MyRecyclerViewAdapter继承RecyclerView.Adapter后,Android Stuido提醒需要重写3个方法,在重写3个方法前,一般会先定义一个Holder继承RecycelrView.ViewHolder,之后直接在MyRecyclerViewAdapter上,指定泛型就是RecyclerHolder

3个需要必须重写的方法:

方法1:public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)

方法2:public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)

方法3:public int getItemCount()

在指定了泛型为RecyclerHoler后,方法2也会根据泛型改变onBindViewHolder(RecyclerHolder holder, int position)

1.1 onCreateViewHolder(ViewGroup parent, int viewType)创建Holder

源码:

/**
 * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent an item.
 * 
 * @param parent The ViewGroup into which the new View will be added after it is bound to an adapter position.
 * @param viewType The view type of the new View.
 *
 * @return A new ViewHolder that holds a View of the given view type.
 */
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
  • ViewGroup parent:可以简单理解为item的根ViewGroupitem的子控件加载在其中
  • int viewTypeitem的类型,可以根据viewType来创建不同的ViewHolder,来加载不同的类型的item

这个方法就是用来创建出一个新的ViewHolder,可以根据需求的itemType,创建出多个ViewHolder。创建多个itemType时,需要getItemViewType(int position)方法配合

1.2 onBindViewHolder(RecyclerHolder holder, int position)绑定ViewHolder

源码:

**
*Called by RecyclerView to display the data at the specified position.
*This method should update the contents of the {@link ViewHolder#itemView} to reflect the item at the given position.
* 
*@param holder The ViewHolder which should be updated to represent the contents of the item at the given position in the data set.
*@param position The position of the item within the adapter's data set.
*/

public abstract void onBindViewHolder(VH holder, int position);
  • VH holder:就是在onCreateViewHolder()方法中,创建的ViewHolder
  • int positionitem对应的DataList数据源集合的postion

postion就是adapter positionRecycelrViewitem的数量,就是根据DataList数据源集合的数量来创建的

1.3 getItemCount()获取Item的数目

源码:

/**
 * Returns the total number of items in the data set held by the adapter.
 *
 * @return The total number of items in this adapter.
 */
public abstract int getItemCount();

这个方法的返回值,便是RecyclerView中实际item的数量。有些情况下,当增加了HeaderView或者FooterView后,需要注意考虑这个返回值

1.4 简单shi yong

一个最简单的RecyclerViewAdapter

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.RecyclerHolder> {
    private Context mContext;
    private List<String> dataList = new ArrayList<>();

    public MyRecyclerViewAdapter(RecyclerView recyclerView) {
        this.mContext = recyclerView.getContext();
    }

    public void setData(List<String> dataList) {
        if (null != dataList) {
            this.dataList.clear();
            this.dataList.addAll(dataList);
            notifyDataSetChanged();
        }
    }

    @Override
    public RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);
        return new RecyclerHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerHolder holder, int position) {
        holder.textView.setText(dataList.get(position));
    }

    @Override
    public int getItemCount() {
        return dataList.size();
    }

    class RecyclerHolder extends RecyclerView.ViewHolder {
        TextView textView;

        private RecyclerHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.tv__id_item_layout);
        }
    }
}

我的个人习惯是单独使用一个setData()方法将DataList传递进Adapter,看到网上有一些博客中会通过构造方法传递。我一般会在网络请求前就初始化Adapter,当异步网络请求拿到解析过的JSON数据后,调用这个方法将数据加载进Adapter,即使做了分页,也可以比较方。但感觉这种方法终究会浪费一点性能

注意,在 onCreateViewHolder()方法中:

View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);

inflate()方法使用的是3个参数的方法。

1.4.1 问题

以前使用2个参数的方法inflate(@LayoutRes int resource, @Nullable ViewGroup root),下面的形式

View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout,null);

遇到的一个问题

《Android——RecyclerView入门学习之RecyclerView.Adapter(三)》 2个参数方法遇到的问题

两种方法,使用的是同一套布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv__id_item_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent"
        android:textAllCaps="false"
        android:textColor="@android:color/white"
        android:textSize="20sp" />

</LinearLayout>

item内的TextView的宽是match_parent,但使用两个参数的方法时,看起来却是wrap_content的效果

1.4.2尝试从源码中找问题

两个参数的方法源码

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
}

两个参数的方法内部调用了3个参数的方法,此时inflate(resourceId, null, false)rootnullattachToRootfalse

最终来到了这里,只保留了部分代码:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ...
    
    View result = root;
    
    ...
    
    // Temp is the root view that was found in the xml
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

    ViewGroup.LayoutParams params = null;
    ...
    
    if (root != null) {
    
        ...
        
        params = root.generateLayoutParams(attrs);
        
        ...               
    }
    
    ...
    
    rInflateChildren(parser, temp, attrs, true);
    
    ...
    
    if (root == null || !attachToRoot) {
        result = temp;
    }
 }

使用两个参数的inflate()方法,ViewGroup.LayoutParams params最终为null;而如果使用3个参数的方法,最终params = params = root.generateLayoutParams(attrs)

这里为了减少出现问题的出现,就使用3个参数的方法inflate(R.layout.id_rv_item_layout, parent, false)

这里看源码也就看了这小段一段,inflate()方法的完整过程还是比较复杂的,比较浅显的知道问题出在哪里后,没有深挖

1.4 点击事件

完整代码:

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.RecyclerHolder> {
    private Context mContext;
    private List<String> dataList = new ArrayList<>();
    private onRecyclerItemClickerListener mListener;

    public MyRecyclerViewAdapter(RecyclerView recyclerView) {
        this.mContext = recyclerView.getContext();
    }


    /**
     * 增加点击监听
     */
    public void setItemListener(onRecyclerItemClickerListener mListener) {
        this.mListener = mListener;
    }

    /**
     * 设置数据源
     */
    public void setData(List<String> dataList) {
        if (null != dataList) {
            this.dataList.clear();
            this.dataList.addAll(dataList);
            notifyDataSetChanged();
        }
    }

    public List<String> getDataList() {
        return dataList;
    }

    @Override
    public RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);
        // View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout,null);
        return new RecyclerHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerHolder holder, int position) {
        holder.textView.setText(dataList.get(position));
        holder.textView.setOnClickListener(getOnClickListener(position));
    }

    private View.OnClickListener getOnClickListener(final int position) {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != mListener && null != v) {
                    mListener.onRecyclerItemClick(v, dataList.get(position), position);
                }
            }
        };
    }


    @Override
    public int getItemCount() {
        return dataList.size();
    }

    class RecyclerHolder extends RecyclerView.ViewHolder {
        TextView textView;

        private RecyclerHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.tv__id_item_layout);
        }
    }

    /**
     * 点击监听回调接口
     */
    public interface onRecyclerItemClickerListener {
        void onRecyclerItemClick(View view, Object data, int position);
    }
}

定义一个接口onRecyclerItemClickerListener,这样可以在Actiivty设置监听对象,之后为TextView设置点击监听事件,在TextView的点击事件方法中,使用onRecyclerItemClick()进行回调

在Activity中使用:

//设置点击事件
adapter.setItemListener(new MyRecyclerViewAdapter.onRecyclerItemClickerListener() {
    @Override
    public void onRecyclerItemClick(View view, Object data, int position) {
        String s = (String) data;
        adapter.getDataList().set(position, s + "---->hi");
            adapter.notifyItemChanged(position);
        }
});

在监控对象回调方法中,使用了notifyItemChanged(position)来进行局部刷新

《Android——RecyclerView入门学习之RecyclerView.Adapter(三)》 点击进行局部刷新

但这种方式会new出一大堆View.OnClickListener,还有一种思路是利用RecycelrViewonTouchListenerGestureDetector手势来进行设置,可以看看三种方式实现RecyclerView的Item点击事件

1.5 一系列的notifyData方法

一共有10个方法

方法作用
notifyDataSetChanged()通知RecycelrView进行全局刷新
notifyItemChanged(int position)通知RecycelrViewadapter position处局进行部刷新
notifyItemRemoved(int position)通知RecyclerView移除在adapter position处的item
notifyItemMoved(int fromPosition, int toPosition)通知RecyclerView移除从fromPositiontoPositionitem
notifyItemRangeRemoved(int positionStart, int itemCount)通知RecyclerView移除从positionStart开始的itemCountitem
notifyItemChanged(int position, Object payload)通知RecyclerView改变指定positionitemobject
notifyItemRangeChanged(int positionStart,int itemCount)通知RecyclerViewpositionStart开始改变itemCountitem
notifyItemRangeChanged(int positionStart,int itemCount,Object payload)通知RecyclerViewpositionStart开始改变itemCountitem的对象
notifyItemInserted(int position)通知RecyclerViewposition处插入一个item
notifyItemRangeInserted(int positionStart, int itemCount)通知RecyclerViewpositionStart开始插入itemCountitem

有些情况下,方法需要考虑组合使用,否则可能出现position错乱,例如

在Adapter中移除或者插入item

/**
 * 移除指定Position的Item
 */
public void remove(int position) {
    if (dataList.size() == 0) return;
    dataList.remove(position);
    notifyItemRemoved(position);
    notifyItemRangeChanged(position, dataList.size() - position);
}

//在Activity中使用 ,设置点击事件
adapter.setItemListener(new MyRecyclerViewAdapter.onRecyclerItemClickerListener() {
    @Override
    public void onRecyclerItemClick(View view, Object data, int position) {
        adapter.remove(position);
        // String s = (String) data;
        // adapter.inserted(position,s+"---->hi");
    }
});

//在指定位置插入一个item
public void inserted(int position, String s) {
    if (dataList.size() == 0) return;
    dataList.add(position, s);
    notifyItemInserted(position);
    notifyItemRangeChanged(position, dataList.size() - position);
}

notifyItemRemoved(position)虽然通知移除了RecycelrViewposition位置上的itemA,但itemA之后的一系列item也需要进行改变,也需要通知RecyclerView进行改变

但这两个方法性能上都有问题,卡顿比较明显,应该会有更好的动态改变或者动态插入item的方法,以后学到了再补充

1.5 简易的封装

通用的ViewHolder:

public class BaseViewHolder extends RecyclerView.ViewHolder {
    private final SparseArray<View> sparseArray;

    public BaseViewHolder(View itemView) {
        super(itemView);
        this.sparseArray = new SparseArray<>(8); //一般一个Item 不会超过8种控件
    }

    public <T extends View> T getView(int viewId) {
        View view = sparseArray.get(viewId);
        if (view == null) {
            view = itemView.findViewById(viewId);
            sparseArray.put(viewId, view);
        }
        return (T) view;
    }

    public BaseViewHolder setText(int viewId, String text) {
        TextView tv = getView(viewId);
        if (tv != null) {
            tv.setText(text);
        }
        return this;
    }
}

主要思路就是使用SparseArray<View>将控件存起来

适配器:

public abstract class CommonBaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
    protected List<T> data = new ArrayList<>();
    protected int itemLayoutId;
    protected Context mContext;
    private onRecyclerItemClickerListener mListener;

    public CommonBaseAdapter(RecyclerView rv, @LayoutRes int itemLayoutId) {
        this.itemLayoutId = itemLayoutId;
        this.mContext = rv.getContext();
    }

    public void setData(List<T> data) {
        if (data != null) {
            this.data.clear();
            this.data.addAll(data);
            notifyDataSetChanged();
        }
    }

    /**
     *  增加点击监听
     */
    public void setItemListener(onRecyclerItemClickerListener mListener) {
        this.mListener = mListener;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //这里使用3个参数的方法
        View view = LayoutInflater.from(mContext).inflate(itemLayoutId, parent, false);
        return new BaseViewHolder(view);
    }

    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        bindViewData(holder, data.get(position), position);
        holder.itemView.setOnClickListener(getOnClickListener(position));
    }

    private View.OnClickListener getOnClickListener(final int position) {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null && v != null) {
                    mListener.onRecyclerItemClick(v, data.get(position), position);
                }
            }
        };
    }

    @Override
    public int getItemCount() {
        return this.data.size();
    }

    public abstract void bindViewData(BaseViewHolder holder, T item, int position);

    interface onRecyclerItemClickerListener {
        void onRecyclerItemClick(View view, Object data, int position);
    }
}

内部提供一个抽象方法bindViewData(),子类重写抽象方法来做一些具体的操作。

封装的很简单,但平常学习使用也能减少一些重复代码。网上有很多强大的封装,可以再深入学习一下为RecyclerView打造通用Adapter让RecyclerView更加好用

使用:

public class RecyclerViewAdapter extends CommonBaseAdapter<String> {

    public RecyclerViewAdapter(RecyclerView rv, @LayoutRes int itemLayoutId, @IdRes int resId) {
        super(rv, itemLayoutId);

    }

    @Override
    public void bindViewData(BaseViewHolder holder, String item, int position) {
        holder.setText(R.id.textViewId, item);
    }
}

Activity中就可以进行使用

这个简易的封装并没有对添加加载图片的方法。加载图片的方法一开始也我封装在了这个CommonBaseAdapter中,但后来发现直接封装在这里并不是好的思路

图片需要做的处理比较多,而且主流的库有3个,为了易于维护,还是将图片的操作单独再封装在一个工具类中,在CommonBaseAdapter中使用操作图片的工具类比较好

1.6 添加HeaderView和FooterViewiew

学的鸿洋大神的代码和思路,涉及到了装饰模式。还有一种添加方式是直接通过使用多种item直接在现有的CommonBaseAdapter来修改,但感觉这种思路需要对CommonBaseAdapter改动的代码太多,点击事件的position也需要考虑,不如鸿洋大神的这种思路易于开发和维护

代码:

public class HeaderAndFooterAdapter extends RecyclerView.Adapter<BaseViewHolder> {
    private CommonBaseAdapter mAdapter;

    private static final int HEADER_VIEW_TYPE = 2 << 6;
    private static final int FOOTER_VIEW_TYPE = 2 << 5;

    private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
    private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();

    public HeaderAndFooterAdapter(CommonBaseAdapter mAdapter) {
        this.mAdapter = mAdapter;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (null != mHeaderViews.get(viewType)) {
            return new HeaderAndFooterHolder(mHeaderViews.get(viewType));
        } else if (null != mFooterViews.get(viewType)) {
            return new HeaderAndFooterHolder(mFooterViews.get(viewType));
        }
        return mAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        if (isHeaderViewPosition(position)) return;
        if (isFooterViewPosition(position)) return;
        mAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderViewPosition(position)) {
            return mHeaderViews.keyAt(position);
        } else if (isFooterViewPosition(position)) {
            return mFooterViews.keyAt(position-getHeaderViewCount()-getAdapterItemCount());
        }
        return mAdapter.getItemViewType(position - getHeaderViewCount());
    }

    @Override
    public int getItemCount() {
        return getHeaderViewCount() + getFooterViewCount() + getAdapterItemCount();
    }

    /**
     * 加入HeaderView
     */
    public void addHeaderView(View view) {
        mHeaderViews.put(mHeaderViews.size() + HEADER_VIEW_TYPE, view);
    }

    /**
     * 加入FooterView
     */
    public void addFootView(View view) {
        mFooterViews.put(mFooterViews.size() + FOOTER_VIEW_TYPE, view);
    }

    /**
     * HeaderView 的数目
     */
    public int getHeaderViewCount() {
        return mHeaderViews.size();
    }

    /**
     * FooterView 的数目
     */
    public int getFooterViewCount() {
        return mFooterViews.size();
    }

    /**
     * 是不是HeaderView的Position
     */
    private boolean isHeaderViewPosition(int position) {
        return position < getHeaderViewCount();
    }


    /**
     * 是不是FooterView的Position
     */
    private boolean isFooterViewPosition(int position) {
        return position >= getHeaderViewCount() + getAdapterItemCount();
    }

    /**
     * 得到Adapter中Item的数目
     */
    private int getAdapterItemCount() {
        return mAdapter.getItemCount();
    }

    private class HeaderAndFooterHolder extends BaseViewHolder {
        private HeaderAndFooterHolder(View itemView) {
            super(itemView);
        }
    }
}

封装的思路:
add进来的view进行保存,当加载item时,利用itemTypeview进行类型判断,如果是HeaderView或者FooterView就创建HeaderAndFooterHolder,然后绑定只是用来显示并没有对HeaderView或者FooterView做其他更多事件的处理

使用也比较方便:

//数据适配器
RecyclerViewAdapter adapter = new RecyclerViewAdapter(rv, R.layout.id_rv_item_layout, R.id.tv__id_item_layout);
//头View适配器
HeaderAndFooterAdapter headerAndFooterAdapter = new HeaderAndFooterAdapter(adapter);
//HeaderView
TextView headerView = new TextView(this);
headerView.setBackgroundColor(Color.BLACK);
headerView.setTextColor(Color.WHITE);
headerView.setWidth(1080);
headerView.setTextSize(50);
headerView.setText("我是头");
headerAndFooterAdapter.addHeaderView(headerView);
//设置适配器
rv.setAdapter(headerAndFooterAdapter);
//添加数据
addData(adapter);

《Android——RecyclerView入门学习之RecyclerView.Adapter(三)》 添加HeaderView

RecyelrView设置的适配器是headerAndFooterAdapter,而添加数据使用的是adapterHeaderAndFooterAdapter是支持添加多个HeaderView

1.6.1 GridLayoutManger和StaggeredGridLayoutManager跨列问题

上面添加HeaderView时,使用的LinerLayoutManager,当使用GridLayoutManger时,便会有问题

HeaderView不能单独占据一行

《Android——RecyclerView入门学习之RecyclerView.Adapter(三)》 GridLayoutManager遇到问题

加入针对GridLayoutManager跨列处理的代码:

/**
 *当RecyelrView开始观察Adapter会被回调
 */
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    mAdapter.onAttachedToRecyclerView(recyclerView);
    final RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
    if (manager instanceof GridLayoutManager) {
        final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
        gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                int viewType = getItemViewType(position);
                //如果是HeaderView或者是FooterView,设置占据gridLayoutManager.getSpanCount()列
                if (null != mHeaderViews.get(viewType) || null != mFooterViews.get(viewType)) {
                    return gridLayoutManager.getSpanCount();
                }
                return 1;
            }
        });
    }
}

《Android——RecyclerView入门学习之RecyclerView.Adapter(三)》 跨列处理

当布局管理器为
GridLayouManger时,对当前要的添加的
item进行判断,如果是HeaderView或者是FooterView,就进行跨列处理,单独占据一行

加入针对StaggeredGridLayoutManager跨列处理的代码:

/**
 * 一个item通过adapter开始显示会被回调
 */
@Override
public void onViewAttachedToWindow(BaseViewHolder holder) {
    super.onViewAttachedToWindow(holder);
    int position = holder.getLayoutPosition();
    if (isHeaderViewPosition(position)||isFooterViewPosition(position)){
        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
        if (null != lp && lp instanceof StaggeredGridLayoutManager.LayoutParams){
            StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
            p.setFullSpan(true);//占满一行
        }
    }
}

《Android——RecyclerView入门学习之RecyclerView.Adapter(三)》 瀑布流跨列

当布局管理器为
StaggeredGridLayoutManager时,对当前要的添加的
item进行判断,如果是HeaderView或者是FooterView,就设置
setFullSpan(true),占满一行

2. 最后

RecyclerView.Adapter暂时大致就学习这些

本人很菜,有错误请指出

共勉 :)

    原文作者:英勇青铜5
    原文地址: https://www.jianshu.com/p/b2e6ad1af557
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞