列表系列程序员今日看点

Android——RecyclerView入门学习之Recycl

2016-10-21  本文已影响5434人  英勇青铜5

学习资料:

之前使用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);

这个方法就是用来创建出一个新的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);

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);

遇到的一个问题

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)来进行局部刷新

点击进行局部刷新

但这种方式会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);
添加HeaderView

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


1.6.1 GridLayoutManger和StaggeredGridLayoutManager跨列问题

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

HeaderView不能单独占据一行

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;
            }
        });
    }
}
跨列处理
当布局管理器为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);//占满一行
        }
    }
}
瀑布流跨列
当布局管理器为StaggeredGridLayoutManager时,对当前要的添加的item进行判断,如果是HeaderView或者是FooterView,就设置setFullSpan(true),占满一行

2. 最后

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

本人很菜,有错误请指出

共勉 :)

上一篇 下一篇

猜你喜欢

热点阅读