zoukankan      html  css  js  c++  java
  • 21、Android--RecyclerView

    RecycleView

    RecyclerView是谷歌V7包下新增的控件,用来替代ListView的使用,它提供了一种插拔式的体验,高度的解耦,异常的灵活

    控制其显示的方式,请通过布局管理器LayoutManager
    控制Item间的间隔(可绘制),请通过ItemDecoration
    控制Item增删的动画,请通过ItemAnimator
    控制点击、长按事件需要自己实现

    首先要用这个控件,你需要在gradle文件中添加包的引用(和V7包版本必须一致):

    compile 'com.android.support:appcompat-v7:25.3.0'
    compile 'com.android.support:recyclerview-v7:25.3.0'

    然后是在布局文件用使用它:

    <android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recycler_view"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"/>
    

    目前都使用AndroidX包下的RecyclerView,需要添加如下依赖:

    implementation 'androidx.recyclerview:recyclerview:1.1.0'

    然后在布局文件中使用它:

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="none"/>
    

    基本使用

    由于我们很熟悉ListView,下面对照ListView来看下RecycleView的使用:

    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    // 设置布局管理器
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    // 设置适配器
    mRecyclerView.setAdapter(mAdapter);
    // 设置Item动画
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    // 添加分割线
    mRecyclerView.addItemDecoration(new DividerItemDecoration(getApplicationContext(), DividerItemDecoration.HORIZONTAL));
    

    可以看出,RecycleView只负责回收和复用View条目,其他的比如分割线和动画都已经由专门的类去实现,以此达到高度解耦。下面我们来看看RecycleView的基本实现:

    1、首先实现页面布局文件和Item条目的布局文件

    // activity_main布局文件
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.legend.recycle.MainActivity">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_recycleview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </RelativeLayout>
    
    // item布局文件
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:background="#44ff0000"
        android:layout_height="wrap_content" >
        <TextView
            android:id="@+id/tv_item_content"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center"
            android:text="1" />
    </FrameLayout>
    

    2、接下来是Activity的代码实现:

    public class MainActivity extends AppCompatActivity {
        private RecyclerView mRecyclerView;
        private List<String> mDatas;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mRecyclerView = (RecyclerView) findViewById(R.id.rv_recycleview);
            initData();
            // 设置布局管理器
            mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
            // 设置适配器
            mRecyclerView.setAdapter(new RecycleAdapter());
        }
        protected void initData() {
            mDatas = new ArrayList<String>();
            for (int i = 'A'; i < 'z'; i++)
            {
                mDatas.add("" + (char) i);
            }
        }
    }
    

    3、最后创建适配器,代码如下所示:

    // 创建适配器
    public class RecycleAdapter extends RecyclerView.Adapter {
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(MainActivity.this).inflate(
                R.layout.item_recycleview, parent, false);
            MyViewHold hold = new MyViewHold(view);
            return hold;
        }
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            ((MyViewHold)holder).mTvContent.setText(mDatas.get(position));
        }
        @Override
        public int getItemCount() {
            return mDatas.size();
        }
        class MyViewHold extends RecyclerView.ViewHolder {
            TextView mTvContent;
            public MyViewHold(View itemView) {
                super(itemView);
                mTvContent = (TextView) itemView.findViewById(R.id.tv_item_content);
            }
        }
    }
    

    显示效果如下:

    ItemDecoration

    我们可以通过addItemDecoration()给RecycleView添加分割线,该方法的参数为RecyclerView.ItemDecoration,该类为抽象类,它的源码如下:

    public static abstract class ItemDecoration {
    public void onDraw(Canvas c, RecyclerView parent, State state) {
          onDraw(c, parent);
     }
    public void onDrawOver(Canvas c, RecyclerView parent, State state) {
          onDrawOver(c, parent);
     }
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
          getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), parent);
    }
    @Deprecated
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
          outRect.set(0, 0, 0, 0);
     }
    

    当我们调用mRecyclerView.addItemDecoration()方法添加decoration时,RecyclerView会去绘制decorato,即调用该类的onDrawonDrawOver方法,

    onDraw方法先于drawChildren
    onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。
    getItemOffsets 可以通过outRect.set()为每个Item设置一定的偏移量,主要用于绘制Decorator。

    接下来我们看下如何给RecycleView添加Item间隔:

    1、 首先创建类继承RecyclerView.ItemDecoration,并实现相关逻辑操作

    public class DividerItemDecoration extends RecyclerView.ItemDecoration {
        private static final int[] ATTRS = new int[]{
                android.R.attr.listDivider
        };
        public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
        public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
        private int mOrientation;
        private final Drawable mDrawable;
        public DividerItemDecoration(Context context, int oritation) {
            TypedArray array = context.obtainStyledAttributes(ATTRS);
            mDrawable = array.getDrawable(0);
            array.recycle();
            setOrientation(oritation);
        }
        public void setOrientation(int orientation) {
            if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
                throw new IllegalArgumentException("invalid orientation");
            }
            mOrientation = orientation;
        }
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            if (mOrientation == VERTICAL_LIST)
                drwaVertical(c, parent);
            else
                drwaHorizontal(c, parent);
        }
        private void drwaVertical(Canvas c, RecyclerView parent) {
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            int top = 0;
            int bottom = 0;
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
                top = child.getBottom() + params.bottomMargin;
                bottom = top + mDrawable.getIntrinsicHeight();
                mDrawable.setBounds(left, top, right, bottom);
                mDrawable.draw(c);
            }
        }
        private void drwaHorizontal(Canvas c, RecyclerView parent) {
            int left = 0;
            int right = 0;
            int top = parent.getPaddingTop();
            int bottom = parent.getHeight() - parent.getPaddingBottom();
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
                left = child.getRight() + params.rightMargin;
                right = left + mDrawable.getIntrinsicHeight();
                mDrawable.setBounds(left, top, right, bottom);
                mDrawable.draw(c);
            }
        }
        @Override
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            if (mOrientation == VERTICAL_LIST)
                outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight());
            else
                outRect.set(0, 0, mDrawable.getIntrinsicWidth(), 0);
        }
    }
    

    2、接下来只要修改Activity中的添加分割线的代码即可:

    mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
    

    运行效果如下所示:

    该分割线是系统默认的,你可以在theme.xml中找到该属性的使用情况。使用系统的listDivider的好处就是方便我们去随意的改变,该属性我们可以直接声明在:

     <!-- Application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">
       <item name="android:listDivider">@drawable/divider_bg</item>  
    </style>
    

    然后自己编写一个Drawable即可,这样可以通过Drawable给分割线设置各种不同的效果:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
           android:shape="rectangle" >
        <gradient
            android:centerColor="#ff00ff00"
            android:endColor="#ff0000ff"
            android:startColor="#ffff0000"
            android:type="linear" />
        <size android:height="4dp"/>
    </shape>
    

    运行显示效果如下:

    LayoutManager

    RecyclerView.LayoutManager一个抽象类,其中系统提供了3个实现类:

    LinearLayoutManager 现行管理器,支持横向、纵向。
    GridLayoutManager 网格布局管理器
    StaggeredGridLayoutManager 瀑布就式布局管理器

    LinerLayoutManager上面的教程已经进行叙述过,接下来介绍网格布局管理器和瀑布式布局管理器。

    GridLayoutManager

    修改上方代码的LayoutManager为GridLayoutManager即可

    // 设置布局管理器
    mRecyclerView.setLayoutManager(new GridLayoutManager(this, 4));
    

    修改为GridLayoutManager以后,分割线也需要修改为适合网格的分割线:

    public class DividerGridItemDecoration extends RecyclerView.ItemDecoration {
        private static final int[] ATTRS = new int[] { android.R.attr.listDivider };
        private Drawable mDivider;
        public DividerGridItemDecoration(Context context) {
            final TypedArray a = context.obtainStyledAttributes(ATTRS);
            mDivider = a.getDrawable(0);
            a.recycle();
        }
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            drawHorizontal(c, parent);
            drawVertical(c, parent);
        }
        private int getSpanCount(RecyclerView parent) {
            // 列数
            int spanCount = -1;
            RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                spanCount = ((StaggeredGridLayoutManager) layoutManager)
                        .getSpanCount();
            }
            return spanCount;
        }
        public void drawHorizontal(Canvas c, RecyclerView parent) {
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                        .getLayoutParams();
                final int left = child.getLeft() - params.leftMargin;
                final int right = child.getRight() + params.rightMargin
                        + mDivider.getIntrinsicWidth();
                final int top = child.getBottom() + params.bottomMargin;
                final int bottom = top + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
        public void drawVertical(Canvas c, RecyclerView parent) {
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                        .getLayoutParams();
                final int top = child.getTop() - params.topMargin;
                final int bottom = child.getBottom() + params.bottomMargin;
                final int left = child.getRight() + params.rightMargin;
                final int right = left + mDivider.getIntrinsicWidth();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
        private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) {
            RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                // 如果是最后一列,则不需要绘制右边
                if ((pos + 1) % spanCount == 0){
                    return true;
                }
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                int orientation = ((StaggeredGridLayoutManager) layoutManager)
                        .getOrientation();
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    // 如果是最后一列,则不需要绘制右边
                    if ((pos + 1) % spanCount == 0) {
                        return true;
                    }
                } else {
                    childCount = childCount - childCount % spanCount;
                    if (pos >= childCount)// 如果是最后一列,则不需要绘制右边
                        return true;
                }
            }
            return false;
        }
        private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) {
            RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                childCount = childCount - childCount % spanCount;
                if (pos >= childCount)// 如果是最后一行,则不需要绘制底部
                    return true;
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                int orientation = ((StaggeredGridLayoutManager) layoutManager)
                        .getOrientation();
                // StaggeredGridLayoutManager 且纵向滚动
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    childCount = childCount - childCount % spanCount;
                    // 如果是最后一行,则不需要绘制底部
                    if (pos >= childCount)
                        return true;
                } else {    // StaggeredGridLayoutManager 且横向滚动
                    // 如果是最后一行,则不需要绘制底部
                    if ((pos + 1) % spanCount == 0) {
                        return true;
                    }
                }
            }
            return false;
        }
        @Override
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            int spanCount = getSpanCount(parent);
            int childCount = parent.getAdapter().getItemCount();
            // 如果是最后一行,则不需要绘制底部
            if (isLastRaw(parent, itemPosition, spanCount, childCount)) {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
                // 如果是最后一列,则不需要绘制右边
            } else if (isLastColum(parent, itemPosition, spanCount, childCount)) {
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(),
                        mDivider.getIntrinsicHeight());
            }
        }
    }
    

    主要在getItemOffsets方法中,去判断如果是最后一行,则不需要绘制底部;如果是最后一列,则不需要绘制右边,整个判断也考虑到了StaggeredGridLayoutManager的横向和纵向,如果仅仅希望有空隙,设置item的margin最方便。

    改变指定位置列尺寸

    如果想改变指定位置的列尺寸,可以使用如下方式:

    假设希望第1个item,单独占据一行的全部空间。我们可以使用setSpanSizeLookup方法实现这一需求:

    GridLayoutManager layoutManager = new GridLayoutManager(this, 4);
    mRecyclerView.setLayoutManager(layoutManager);
    layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    	@Override
    	public int getSpanSize(int position) {
    		if (position == 0){
    			return 4;
    		}
    		return 1;
    	}
    });
    

    这样第一行单独占据整行空间,而其他列还是以四个网格铺满的方式呈现

    StaggereGridLayoutManage

    瀑布流式的布局,其实他可以实现GridLayoutManager一样的功能,仅仅按照下列代码:

    // 设置布局管理器
    mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.VERTICAL));
    

    StaggeredGridLayoutManager构造的第二个参数传一个orientation,如果传入的是StaggeredGridLayoutManager.VERTICAL代表有多少列;

    那么传入的如果是StaggeredGridLayoutManager.HORIZONTAL就代表有多少行。

    如果想实现瀑布流效果,只需要在适配器中onBindViewHolder方法设置item随机高度即可:

    @Override
    public void onBindViewHolder(MyViewHold holder, int position) {
        // 设置随机高度
        int randomInt = (int)(300+Math.random()*400);
        ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
        layoutParams.height = randomInt;
        holder.itemView.setLayoutParams(layoutParams);
        
        // 设置显示
        holder.mContent.setText(mListData.get(position));
    }
    

    ItemAnimator

    ItemAnimator也是一个抽象类,系统提供了默认的实现类:

    // 设置item动画
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    

    使用动画后,更新数据集使用 notifyItemInserted(position)notifyItemRemoved(position) ,否则没有动画效果。

    点击事件处理

    RecycleView中系统没有提供ClickListener和LongClickListener,需要我们自己在适配器中去实现。

    public class LinearRecycleAdapter extends RecyclerView.Adapter<LinearRecycleAdapter.MyViewHold> {
        private Context mContext;
        private List<String> mListData;
        private OnItemClickLitener mOnItemClickLitener;
        public LinearRecycleAdapter(Context context, List<String> listData) {
            this.mContext = context;
            this.mListData = listData;
        }
        public interface OnItemClickLitener {
            void onItemClick(View view, int position);
            void onItemLongClick(View view , int position);
        }
        @Override
        public MyViewHold onCreateViewHolder(ViewGroup parent, int viewType) {
            MyViewHold viewHold = new MyViewHold(LayoutInflater.from(mContext)
                    .inflate(R.layout.item_recycle_single, parent, false));
            return viewHold;
        }
        @Override
        public void onBindViewHolder(final MyViewHold holder, int position) {
            holder.mContent.setText(mListData.get(position));
            // 如果设置了回调,则设置点击事件
            if (mOnItemClickLitener != null) {
                holder.itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        int pos = holder.getLayoutPosition();
                        mOnItemClickLitener.onItemClick(holder.itemView, pos);
                    }
                });
                holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        int pos = holder.getLayoutPosition();
                        mOnItemClickLitener.onItemLongClick(holder.itemView, pos);
                        return false;
                    }
                });
            }
        }
        @Override
        public int getItemCount() {
            return mListData.size();
        }
        static class MyViewHold extends RecyclerView.ViewHolder {
            private TextView mContent;
            public MyViewHold(View itemView) {
                super(itemView);
                mContent = itemView.findViewById(R.id.tv_item_content);
            }
        }
        // 条目点击事件
        public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener) {
            this.mOnItemClickLitener = mOnItemClickLitener;
        }
    }
    

    多样式布局

    RecyclerView多样式布局具体流程如下:

    1、编写基本的Bean

    public class StyleItem {
        private int type;
        private String content;
        private int resId;
        public StyleItem(int type, String content, int resId) {
            this.type = type;
            this.content = content;
            this.resId = resId;
        }
    ......
    }
    

    2、编写适配器

    public class MultiStyleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
        private Context mContext;
        public static final int TYPE_ONE = 0;
        public static final int TYPE_TWO = 1;
        private List<StyleItem> mStyleItems;
        public MultiStyleAdapter(Context context, List<StyleItem> styleItems) {
            this.mContext = context;
            this.mStyleItems = styleItems;
        }
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == TYPE_ONE) {
                View view = LayoutInflater.from(mContext).inflate(R.layout.rectycle_item_style1, parent, false);
                ViewHolderStyle1 viewHolderHead = new ViewHolderStyle1(view);
                return viewHolderHead;
            } else {
                View view = LayoutInflater.from(mContext).inflate(R.layout.rectycle_item_style2, parent,false);
                ViewHolderStyle2 viewHolderHead = new ViewHolderStyle2(view);
                return viewHolderHead;
            }
        }
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (mStyleItems == null && mStyleItems.size() < 0)
                return;
            StyleItem styleItem = mStyleItems.get(position);
            int type = styleItem.getType();
            if (type == TYPE_ONE && holder instanceof ViewHolderStyle1){
                ((ViewHolderStyle1) holder).tvContent.setText(styleItem.getContent());
            }else if (type == TYPE_TWO && holder instanceof ViewHolderStyle2){
                ((ViewHolderStyle2) holder).ivBg.setImageResource(styleItem.getResId());
            }
        }
        @Override
        public int getItemCount() {
            if (mStyleItems != null && mStyleItems.size() > 0)
                return mStyleItems.size();
            return 0;
        }
        @Override
        public int getItemViewType(int position) {
            StyleItem styleItem = mStyleItems.get(position);
            return styleItem.getType();
        }
        class ViewHolderStyle1 extends RecyclerView.ViewHolder {
            TextView tvContent;
            public ViewHolderStyle1(View itemView) {
                super(itemView);
                tvContent = itemView.findViewById(R.id.tv_content);
            }
        }
        class ViewHolderStyle2 extends RecyclerView.ViewHolder {
            ImageView ivBg;
            private ViewHolderStyle2(View itemView) {
                super(itemView);
                ivBg = itemView.findViewById(R.id.iv_icon);
            }
        }
    }
    

    其中使用到的布局文件如下:
    rectycle_item_style1.xml

    <?xml version="1.0" encoding="utf-8"?>
    <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_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/purple_200"
            android:gravity="center"
            android:padding="10dp"
            android:text="Style1" />
    </LinearLayout>
    

    rectycle_item_style2.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <ImageView
            android:id="@+id/iv_icon"
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:gravity="center"
            android:scaleType="fitXY"
            android:src="@drawable/bg" />
    </LinearLayout>
    

    3、在Activity中的使用方式如下:

    public class MainActivity extends AppCompatActivity {
        private RecyclerView mRecyclerView;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mRecyclerView = findViewById(R.id.recyclerview);
            LinearLayoutManager layoutManager = new LinearLayoutManager(this);
            layoutManager.setOrientation(RecyclerView.VERTICAL);
            mRecyclerView.setLayoutManager(layoutManager);
            List<StyleItem> styleItems = new ArrayList<>();
            for (int i = 0; i < 10; i++){
                int type = i % 2 == 0 ? MultiStyleAdapter.TYPE_ONE : MultiStyleAdapter.TYPE_TWO;
                StyleItem styleItem = new StyleItem(type, "Style" + i, R.drawable.bg);
                styleItems.add(styleItem);
            }
            MultiStyleAdapter adapter = new MultiStyleAdapter(this, styleItems);
            mRecyclerView.setAdapter(adapter);
        }
    }
    

    复用错位

    RecyclerView在展示大量的控件和内容的时候,会采取复用的核心机制,所以会导致数据错乱相关的Bug。

    RecyclerView在加载过程中会出现数据来源数同步或异步的情况,这里会分两种情况介绍:

    数据同步情况

    当数据来源数同步的情况下,这种处理方式会比较简单,我们只需要保证在OnBindViewHolder方法调用的时候,ItemView中每个View的状态都有默认值即可。

    假设我们实现RecyclerView中放入CheckBox的需求,就可以使用如下方式来保证CheckBox的选中状态不被复用而导致错乱。

    1、在Activity的布局文件中使用RecyclerView

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scrollbars="none"/>
    </LinearLayout>
    

    2、编写CheckItem.java对象,需要注意的是需要加入select值来保存当前 Item的状态:

    public class CheckItem {
        private String content;
        private boolean select;
        public CheckItem(String content, boolean select) {
            this.content = content;
            this.select = select;
        }
    	......
    }
    

    3、编写RecyclerView的适配器代码如下所示:

    public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.MyViewHold> {
        private Context mContext;
        private List<CheckItem> mCheckItems;
    
        public CustomAdapter(Context context, List<CheckItem> checkItems) {
            this.mContext = context;
            this.mCheckItems = checkItems;
        }
    
        @Override
        public MyViewHold onCreateViewHolder(ViewGroup parent, int viewType) {
            View layout = View.inflate(mContext, R.layout.item_recycler_content, null);
            MyViewHold viewHold = new MyViewHold(layout);
            return viewHold;
        }
    
        @Override
        public void onBindViewHolder(final MyViewHold holder, int position) {
            final CheckItem checkItem = mCheckItems.get(position);
            holder.mTvContent.setText(checkItem.getContent());
            Log.e("CustomAdapter", checkItem.isSelect() + "|" + position);
            if (checkItem.isSelect()){
                holder.mCheckBox.setChecked(true);
            }else {
                holder.mCheckBox.setChecked(false);
            }
            // Event
            holder.mCheckBox.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    boolean checked = holder.mCheckBox.isChecked();
                    checkItem.setSelect(checked);
                }
            });
        }
    
        @Override
        public int getItemCount() {
            return mCheckItems.size();
        }
    
        class MyViewHold extends RecyclerView.ViewHolder {
            TextView mTvContent;
            CheckBox mCheckBox;
            public MyViewHold(View itemView) {
                super(itemView);
                mTvContent = itemView.findViewById(R.id.tv_recycler_item_content);
                mCheckBox = itemView.findViewById(R.id.cb_recycler_item_check);
            }
        }
    }
    

    每次CheckBox被选中的时候都会在Item中保存CheckBox的选中状态,所以即使ItemView被复用的情况下都不会丢失CheckBox的状态,直接会在'onBindViewHolder'中还原选中状态。

    其中使用到的Item布局文件item_recycler_content.xml如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:padding="5dp">
            <TextView
                android:id="@+id/tv_recycler_item_content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:gravity="center"
                android:textSize="17sp"
                android:text="0000"/>
            <CheckBox
                android:id="@+id/cb_recycler_item_check"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"/>
        </RelativeLayout>
    </LinearLayout>
    

    4、最后在Activity中的代码如下:

    public class MainActivity extends AppCompatActivity {
        private RecyclerView mRecyclerView;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mRecyclerView = findViewById(R.id.recyclerView);
            List<CheckItem> checkItems = new ArrayList<>();
            for (int i = 0; i < 100; i++){
                CheckItem checkItem = new CheckItem("这是选项" + i, false);
                checkItems.add(checkItem);
            }
            mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
            CustomAdapter adapter = new CustomAdapter(this, checkItems);
            mRecyclerView.setAdapter(adapter);
        }
    }
    

    数据异步情况

    在RecyclerView的使用过程中,异步加载图片的场景是非常常见的,由于复用机制会导致图片的加载出现错乱的情况,出现该问题的原因如下:

    当我为第一个ImageView请求图片的时候,手指向下滑动来屏幕到某个位置,此时第一个ItemView已不可见,这个时候某个位置复用第一个位置的ImageView,刚好此时图片加载完成返回,所以就导致加载的图片展示在某个位置上,就引起来图片错乱的问题。

    我们要解决这个问题,主要有三种常用的解决方案:

    第一种方式:使用Glide异步加载图片的时候,使用占位图的方式解决。(Glide其实也是使用绑定Tag的方式)

    第二种方式:为每个ImageView绑定TAG的方式,将ImageView和图片的URL地址进行绑定,在设置图片的时候判断URL是否一致来处理错乱问题。

    第三种方式:在ItemView不可见被复用的时候,如果图片还没有加载完成,则取消该图片的请求。

    设置占位图

    使用Glide设置占位图有两种方式,直接链式调用'placeholder'方法,或者为Glide添加监听器,在回调方法中设置占位图。

    1、第一种设置占位图的方法:

    Glide.with(mContext)
            .load(imageItem.getUrl())
            .placeholder(R.mipmap.ic_launcher)
            .into(holder.imageView);
    

    2、第二种设置占位图的方法:

    Glide.with(mContext)
            .load(imageItem.getUrl())
            .error(R.mipmap.ic_launcher)
            .into(new SimpleTarget<Drawable>() {
                @Override
                public void onResourceReady(Drawable resource, Transition<? super Drawable> transition) {
                    holder.imageView.setImageDrawable(resource);
                }
    
                @Override
                public void onStart() {
                    super.onStart();
                    holder.imageView.setImageResource(R.mipmap.ic_launcher);
                }
            });
    

    由于'SimpleTarget'回调已经过时,在新版的Glide中可以通过listeneraddListener的方式添加监听器。

    Glide.with(mContext)
            .load(imageItem.getUrl())
            .addListener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                    holder.imageView.setImageResource(R.mipmap.ic_launcher);
                    return false;
                }
    
                @Override
                public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                    holder.imageView.setImageDrawable(resource);
                    return false;
                }
            }).into(holder.imageView);
    

    绑定TAG

    使用绑定TAG的方式来处理复用错乱问题的时候,如果图片加载使用Glid的话需要使用setTag(key,value)方式进行设置,因为Glide内部就是通过setTag()的方式设置的。

    @Override
    public void onBindViewHolder(final MyViewHold holder, final int position) {
        final ImageItem imageItem = mImageUrls.get(position);
        holder.imageView.setTag(R.id.glide_custom_view_target_tag, position);
        Glide.with(mContext)
                .load(imageItem.getUrl())
                .error(R.mipmap.ic_launcher)
                .listener(new RequestListener<Drawable>() {
                    @Override
                    public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                        holder.imageView.setImageResource(R.mipmap.ic_launcher);
                        return false;
                    }
                    @Override
                    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                        if (position == holder.imageView.getTag(R.id.glide_custom_view_target_tag)){
                            holder.imageView.setImageDrawable(resource);
                        }
                        return false;
                    }
                }).into(holder.imageView);
    }
    

    取消请求

    在onBindViewHolder中发起加载请求,然后在view被回收时取消网络请求,所以我们需要实现RecyclerView的Adapter中的onViewRecycled()方法,然后在该方法中取消请求即可。

    @Override
    public void onBindViewHolder(final MyViewHold holder, final int position) {
        final ImageItem imageItem = mImageUrls.get(position);
        holder.imageView.setTag(imageItem.getKey(), position);
        Glide.with(mContext)
                .load(imageItem.getUrl())
                .placeholder(R.mipmap.ic_launcher)
                .into(holder.imageView);
    }
    
    @Override
    public void onViewRecycled(MyViewHold holder) {
        if (holder != null){
            Glide.with(mContext).clear(holder.imageView);
            holder.imageView.setImageResource(R.mipmap.ic_launcher);
        }
        super.onViewRecycled(holder);
    }
    

    使用取消图片请求的方式来解决复用导致图片加载错乱的问题是比较方便也比较常用的一种方式,推荐使用。

  • 相关阅读:
    Nancy 寄宿IIS
    原子操作
    CSRF跨站请求伪造
    CORS跨域
    C# 运算符
    Mysql 函数
    Mongodb for .Net Core 驱动的应用
    Mongodb for .Net Core 封装类库
    制作项目模板
    压缩图片
  • 原文地址:https://www.cnblogs.com/pengjingya/p/5509565.html
Copyright © 2011-2022 走看看