【Android UI】RecyclerView

Google 在Android 5.0 提供了material design系列的控件,其中RecyclerView就是其中的一员。相对于ListView而言,RecyclerView功能强大,接下来总结回顾下~

入门篇

1、依赖引入

RecyclerView是material design库里的一员,我们引入material design库即可~

dependencies {
    // material design lib
    implementation 'com.google.android.material:material:1.9.0'
}

此时我们不仅可以使用RecyclerView控件,只要是material design系列的控件都可以使用,比如CardView、MaterialButton、TextInputLayout、BottomNavigationView等。

如果我们只是仅仅使用RecyclerView控件,那么只引入RecyclerView的依赖也行

dependencies {
    //rv lib
    implementation "androidx.recyclerview:recyclerview:1.3.2"
}

2、一个🌰

首先跑个例子,先入门了再说😁

<?xml version="1.0" encoding="utf-8"?>
<layout 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">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".HelloWordActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class HelloWordActivity : AppCompatActivity() {
    private val mBinding: ActivityHelloWordBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.activity_hello_word)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        addWindowInsets()
        setUpRvList()
    }

    private fun addWindowInsets() {
        ViewCompat.setOnApplyWindowInsetsListener(mBinding.root) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
    }

    private fun setUpRvList() {
        val list = arrayListOf("i", "am", "recyclerview", "item")
        mBinding.rvList.run {
            //给recycler view 设置布局管理器,这里设置线性布局管理器
            layoutManager = LinearLayoutManager(this@HelloWordActivity)
            // 给recycler view 设置adapter
            adapter = HelloWorldAdapter(data = list)
        }
    }
}
/**
 * Create by SunnyDay /06/21 22:28:50
 */
data class HelloWorldAdapter(
    val data: List<String>
) : Adapter<HelloWorldViewHolder>() {


   /**
     * 创建ViewHolder
     * @param parent:父容器,指RecyclerView。(通过打印log我们可以加以验证)
     * @param viewType  条目类型。这里可以先忽略不管。多viewType item时我们会用到这个。参见下文多viewType知识点。
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HelloWorldViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: LayoutHelloWorldAdapterBinding = DataBindingUtil.inflate(
            layoutInflater,
            R.layout.layout_hello_world_adapter,
            parent,
            false
        )
        return HelloWorldViewHolder(binding)
    }

    /**
     * RecyclerView item 的数目
     */
    override fun getItemCount() = data.size


    /**
     * 当绑定ViewHolder时回调这个方法,在这里可以处理view的一些逻辑。 
     * @param holder  我们自定义的ViewHolder
     * @param position 对应的item 位置
     *                    
     */
    override fun onBindViewHolder(holder: HelloWorldViewHolder, position: Int) {
        holder.binding.run {
            //处理view的一些逻辑。 
            tvText.text = data[position]
        }
    }

   /**
    * super.getItemViewType(position) 默认返回值为0,代表只有1种item类型。在UI上就是整个页面item都是类似的。
    * 当我们想要某些item中可以展示纯图片、某些item可以展示存文字、某些item是图片居左+文字居右,,,等等就可以重载这个方法。
    */
    override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }
}

data class HelloWorldViewHolder(val binding: LayoutHelloWorldAdapterBinding) :ViewHolder(binding.root)


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tv_text"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/color_gray"
            android:gravity="center"
            android:text="@string/default_text"
            android:textColor="@color/white"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <View
            android:id="@+id/divide_line"
            app:layout_constraintTop_toBottomOf="@id/tv_text"
            android:layout_width="match_parent"
            android:layout_height="5dp"
            app:layout_constraintStart_toStartOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

使用还是蛮简单的,demo 跑起来就算入门啦 😁😁 😁(^-^)V

3、小收获

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HelloWorldViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: LayoutHelloWorldAdapterBinding = DataBindingUtil.inflate(
            layoutInflater,
            R.layout.layout_hello_world_adapter,
            parent,
            false
        )
        return HelloWorldViewHolder(binding)
    }
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

把xml转化为View时,这段代码使用了DataBinding,其本质还是通过LayoutInflater#inflate来转化的。但在RecyclerView的adapter中使用我们必须留意两点:

  • root 参数不能为null,要传递一个parent,否则item不居中
  • attachToRoot参数必须传递false,否则直接crash

这是为啥呢?

不居中是因为在xml转化view时父容器指定为null,代表view没父容器,此时view都不知道自己如何layout,当然会出现位置摆放错误的问题了。

crash又是因为啥呢?这个可以看下日志就明白了

java.lang.IllegalStateException: 

ViewHolder views must not be attached when created. 

Ensure that you are not passing 'true' to the attachToRoot parameter of

LayoutInflater.inflate(..., boolean attachToRoot)

4、生命周期

生命周期这一块是一个重要知识点,在四大组件中我们利用生命周期可以处理一下logic,所以RecyclerView这块生命周期我们也是有必要了解的,接下来我们需要熟悉下Adapter的生命周期回调方法。

data class HelloWorldAdapter(
    val data: List<String>
) : Adapter<HelloWorldViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HelloWorldViewHolder {
        Log.d("HelloWorldAdapter","onCreateViewHolder")
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: LayoutHelloWorldAdapterBinding = DataBindingUtil.inflate(
            layoutInflater,
            R.layout.layout_hello_world_adapter,
            parent,
            false
        )
        return HelloWorldViewHolder(binding)
    }

    override fun getItemCount() = data.size

    override fun onBindViewHolder(holder: HelloWorldViewHolder, position: Int) {
        Log.d("HelloWorldAdapter","onBindViewHolder:${data[position]}")
        holder.binding.run {
            tvText.text = data[position]
        }
    }

    override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        Log.d("HelloWorldAdapter","onAttachedToRecyclerView:")
    }

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
        Log.d("HelloWorldAdapter","onDetachedFromRecyclerView:")
    }

    override fun onViewAttachedToWindow(holder: HelloWorldViewHolder) {
        super.onViewAttachedToWindow(holder)
        Log.d("HelloWorldAdapter","onViewAttachedToWindow:")
    }

    override fun onViewDetachedFromWindow(holder: HelloWorldViewHolder) {
        super.onViewDetachedFromWindow(holder)
        Log.d("HelloWorldAdapter","onViewDetachedFromWindow:")
    }
    
    override fun onViewRecycled(holder: HelloWorldViewHolder) {
        super.onViewRecycled(holder)
        Log.d("HelloWorldAdapter","onViewRecycled:")
    }
}

data class HelloWorldViewHolder(val binding: LayoutHelloWorldAdapterBinding) :
    ViewHolder(binding.root)
  • onCreateViewHolder:创建ViewHolder时回调
  • onBindViewHolder:绑定ViewHolder时回调
  • onAttachedToRecyclerView:RecyclerView 与适配器关联时被调用,也即setAdapter方法会触发onAttachedToRecyclerView。但需要注意,多次setAdapter时onAttachedToRecyclerView会被多次调用,而且是先走detached,然后再Attached。
  • onDetachedFromRecyclerView:adapter与RecyclerView取消关联时回调
  • onViewAttachedToWindow:某一item可见时回调
  • onViewDetachedFromWindow:某一item不可见时回调
  • onViewRecycled:某一item被RecyclerView回收时调用。在这里我们可以进行一些回收工作,最常见的就是对bitmap进行释放

在这里插入图片描述

二、基础篇

1、LayoutManager

在这里插入图片描述

LayoutManager负责RecyclerView的布局管理,其中包含了Item View的获取与回收。常见的LayoutManager有如下几种

(1) LinearLayoutManager

线性布局管理器,管理item的显示效果(垂直或者水平)。

LinearLayoutManager 提供了两个属性LinearLayout.VERTICAL代表item以垂直方向摆放,LinearLayout.HORIZONTAL代表item以水平方向摆放。

看下其提供的构造函数

public class LinearLayoutManager extends RecyclerView.LayoutManager implements
        ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
        
   //定义了两种摆放方式。    
   public static final int HORIZONTAL = RecyclerView.HORIZONTAL;
   public static final int VERTICAL = RecyclerView.VERTICAL;
    
    /**
     *构造函数,定义了三个参数:
     *
     *1、context:Context
     *
     *2、orientation:item摆放方式,有两种HORIZONTAL与VERTICAL
     *
     *3、reverseLayout:boolean类型,表示item是否逆序显示。默认false
     */
    public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
            boolean reverseLayout) {
        setOrientation(orientation);
        setReverseLayout(reverseLayout);
    }
    
    /**
     *一个参数的构造,其内部调用三个参数的构造。
     *orientation写死为RecyclerView.DEFAULT_ORIENTATION,查看代码我们会发现这个值默认为RecyclerView.VERTICAL
     *
     *reverseLayout写死为false,代表item不翻转。
     */
   public LinearLayoutManager(Context context) {    
        this(context, RecyclerView.DEFAULT_ORIENTATION, false);
    }
    
    /*
     *这个构造我们使用较少,当我们在xml中设置LinearLayoutManager布局管理器时会调用这个构造。
     默认orientation = vertical
     */
    public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
        setOrientation(properties.orientation);
        setReverseLayout(properties.reverseLayout);
        setStackFromEnd(properties.stackFromEnd);
    }

}

如何在xml中指定布局管理器呢?如下

       <androidx.recyclerview.widget.RecyclerView
            app:layoutManager="LinearLayoutManager"
            android:orientation="vertical"
            android:id="@+id/rv_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
(2) GridLayoutManager

表格布局管理器,可以实现表格样式效果。

/*
   表格布局管理器
   context:Context
   spanCount:表格的列数或行数。与orientation有关。
             当orientation为LinearLayoutManager.VERTICAL时spanCount代表列数
             当orientation为LinearLayoutManager.HORIZONTAL时spanCount代表行数
            
   reverseLayout:是否反转布局          
*/
public GridLayoutManager(
            Context context,
            int spanCount,
            int orientation,
            boolean reverseLayout) 

看个简单的例子~

data class GridDto(
     val text: String,
     val bgColor: Int
)

private fun colorList() = listOf(
    Color.RED,
    Color.BLUE,
    Color.CYAN,
    Color.GRAY,
    Color.DKGRAY,
    Color.BLACK,
    Color.GREEN,
    Color.LTGRAY,
    Color.MAGENTA,
    Color.YELLOW,
    Color.RED,
    Color.BLUE,
    Color.CYAN,
    Color.GRAY,
    Color.DKGRAY,
    Color.BLACK,
    Color.GREEN,
    Color.LTGRAY,
    Color.MAGENTA,
    Color.YELLOW,
    Color.RED,
    Color.BLUE,
    Color.CYAN,
    Color.GRAY,
    Color.DKGRAY,
    Color.BLACK,
    Color.GREEN,
    Color.LTGRAY,
    Color.MAGENTA,
    Color.YELLOW,
)

fun mockGridDtoList() = colorList().mapIndexed { index, bgColor ->
    GridDto(
        text = index.toString(),
        bgColor = bgColor
    )
}
    private fun setUpGridRvList() {

        val manager = GridLayoutManager(this@LayoutManagerActivity,4,LinearLayoutManager.VERTICAL,false)
        mBinding.rvList.run {
            layoutManager = manager
            adapter = CardAdapter(mockGridDtoList())
        }
    }
data class CardAdapter(val data: List<GridDto>) : Adapter<CardViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: LayoutGridAdapterBinding = DataBindingUtil.inflate(
            layoutInflater,
            R.layout.layout_grid_adapter,
            parent,
            false
        )
        return CardViewHolder(binding)
    }

    override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
        holder.binding.run {
            tvText.text = data[position].text
            tvText.setBackgroundColor(data[position].bgColor)
        }
    }


    override fun getItemCount() = data.size


}

data class CardViewHolder(val binding: LayoutGridAdapterBinding) : ViewHolder(binding.root)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tv_text"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:text="@string/this_is_an_icon"
            android:textColor="@color/white"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

效果如下~

在这里插入图片描述

注意GridLayoutManager 中有个知识点还是比较重要的:spanSizeLookup

这个属性可以设置一个表格独占几个空间,如上图默认情况下每个表格独占1个表格空间。接下来看下第1个元素独占2个空间的效果是怎样的,我们只需这样修改:

        manager.spanSizeLookup = object : SpanSizeLookup() {
            override fun getSpanSize(position: Int): Int {
                return if (position==1){
                    2
                }else{
                    1
                }
              
            }
        }

在这里插入图片描述

看是不是很神奇~

(3)StaggeredGridLayoutManager

可以实现瀑布流效果,接下来看下常用的构造函数

    /**
     * Creates a StaggeredGridLayoutManager with given parameters.
     *
     * @param spanCount   If orientation is vertical, spanCount is number of columns. If
     *                    orientation is horizontal, spanCount is number of rows.
     * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
     */
    public StaggeredGridLayoutManager(int spanCount, int orientation)

这里我们也搞个栗子演示下~

data class StaggeredDto(
    val text: String,
    val bgColor: Int
)

fun mockStaggeredList() = colorList().mapIndexed { index, bgColor ->
    StaggeredDto(
        text = index.toString(),
        bgColor = bgColor
    )
}
    private fun setupStaggeredRvList(){
        val manager = StaggeredGridLayoutManager(4,LinearLayoutManager.VERTICAL)
        mBinding.rvList.run {
            layoutManager = manager
            adapter = StaggeredAdapter(mockStaggeredList())
        }
    }
class StaggeredAdapter(
    val data: List<StaggeredDto>
) : Adapter<StaggeredViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StaggeredViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: LayoutStaggeredAdapterBinding = DataBindingUtil.inflate(
            layoutInflater,
            R.layout.layout_staggered_adapter,
            parent,
            false
        )
        return StaggeredViewHolder(binding)
    }

    override fun getItemCount() = data.size

    override fun onBindViewHolder(holder: StaggeredViewHolder, position: Int) {
        holder.binding.run {
            tvText.text = data[position].text
            tvText.setBackgroundColor(data[position].bgColor)
            // 随机修改每个item的高度,模拟瀑布流效果
            val params = tvText.layoutParams as ConstraintLayout.LayoutParams
            params.height =  (100..1000).random()
            tvText.layoutParams = params
        }
    }
}

data class StaggeredViewHolder(val binding: LayoutStaggeredAdapterBinding) : ViewHolder(binding.root)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tv_text"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:text="@string/this_is_an_icon"
            android:textColor="@color/white"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

在这里插入图片描述

emmm,效果是和GridLayoutManager不一样,但是代码为何及其相似???,如果在CardAdapter中加入类似StaggeredAdapter
中动态设置item高度的代码会怎样???其实我们可以去试下,也能达到类似的效果。

那么问题来了GridLayoutManager与StaggeredGridLayoutManager的区别是啥?

1、布局方式

  • GridLayoutManager每个item的宽度(或高度)通常是固定的,所有的item在同一行(或列)中的尺寸一致。如果你手动改变每个item的宽度或高度,就能模拟出一个类似瀑布流的效果,但需要手动处理每个item的布局参数。
  • StaggeredGridLayoutManager专门为实现不规则网格布局而设计,允许每一列(或行)的item具有不同的高度(或宽度)。它自动处理item的摆放,并确保所有item填满屏幕,从而天然实现瀑布流布局。

2、自动化 vs 手动

  • GridLayoutManager你需要手动调整每个item的尺寸或位置,以实现不规则布局。这可能会增加代码复杂性,尤其是当你要处理很多不同尺寸的内容时。
  • StaggeredGridLayoutManager它自动处理所有item的摆放和尺寸变化,你只需提供数据。布局管理器会根据item的内容动态调整它们的排列方式。

3、 适用场景

  • GridLayoutManager适合于规则网格布局的场景,或者在你需要高度自定义控制的情况下,例如想要精确控制每个item的宽度或高度。
  • StaggeredGridLayoutManager 适用于需要不规则网格布局的场景,比如图片墙、商品展示、新闻应用等,特别是在item高度变化频繁的情况下。它能自动处理不同高度的item,减少你的手动调整。

4、 性能和代码复杂度

  • GridLayoutManager 如果需要大量手动调整布局参数,可能会增加代码复杂度,而且需要谨慎处理性能问题。
  • StaggeredGridLayoutManager通过减少手动调整,可以简化代码,并让布局更灵活。它的布局算法专门为处理瀑布流设计,在性能上更有优势,尤其是在处理大数据集时。

因此如果你正在处理不规则的内容(如不同高度的图片或文本块),StaggeredGridLayoutManager 能更自然、更高效地实现瀑布流布局。而 GridLayoutManager 则更适合于规则网格,或当你需要对每个项目的尺寸和位置进行精细控制时使用。

(4) CarouselLayoutManager

可以实现Banner的效果,其CarouselLayoutManager的构造接受一个strategy,通过传递不同的strategy能实现不同的Carousel效果

在这里插入图片描述

在这里插入图片描述

Github上有个project对这个介绍比较详细,具体可以针对这个了解一番~

2、Item Decoration

RecyclerView的条目默认是没有分割线的,但是官方却暴露了一个接口让用户自定义实现。

    /**
     * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
     * affect both measurement and drawing of individual item views.
     *
     * <p>Item decorations are ordered. Decorations placed earlier in the list will
     * be run/queried/drawn first for their effects on item views. Padding added to views
     * will be nested; a padding added by an earlier decoration will mean further
     * item decorations in the list will be asked to draw/pad within the previous decoration's
     * given area.</p>
     *
     * @param decor Decoration to add
     */
    public void addItemDecoration(@NonNull ItemDecoration decor) {
        addItemDecoration(decor, -1);
    }
    public abstract static class ItemDecoration {
        /**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn before the item views are drawn,
         * and will thus appear underneath the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView
         */
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
            onDraw(c, parent);
        }

        /**
         * @deprecated
         * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
         */
        @Deprecated
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        /**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn after the item views are drawn
         * and will thus appear over the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView.
         */
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                @NonNull State state) {
            onDrawOver(c, parent);
        }

        /**
         * @deprecated
         * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
         */
        @Deprecated
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }


        /**
         * @deprecated
         * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
         */
        @Deprecated
        public void getItemOffsets(@NonNull Rect outRect, int itemPosition,
                @NonNull RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        /**
         * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
         * the number of pixels that the item view should be inset by, similar to padding or margin.
         * The default implementation sets the bounds of outRect to 0 and returns.
         *
         * <p>
         * If this ItemDecoration does not affect the positioning of item views, it should set
         * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
         * before returning.
         *
         * <p>
         * If you need to access Adapter for additional data, you can call
         * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
         * View.
         *
         * @param outRect Rect to receive the output.
         * @param view    The child view to decorate
         * @param parent  RecyclerView this ItemDecoration is decorating
         * @param state   The current state of RecyclerView.
         */
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                @NonNull RecyclerView parent, @NonNull State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

大致看了下有两个重要的方法

  • onDraw:实现分割线绘制
  • getItemOffsets:实现设置item的偏移量

既然让我们自己实现,我们只需继承此类,重写这两个方法即可。在此之前我们学习下官方提供的默认分割线,看看人家是如何实现的:


/**
 * DividerItemDecoration is a {@link RecyclerView.ItemDecoration} that can be used as a divider
 * between items of a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and
 * {@link #VERTICAL} orientations.
 *
 * <pre>
 *   mDividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
 *   mLayoutManager.getOrientation());
 *   recyclerView.addItemDecoration(mDividerItemDecoration);
 * </pre>
 */
public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
    public static final int VERTICAL = LinearLayout.VERTICAL;

    private static final String TAG = "DividerItem";
    private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };

    private Drawable mDivider;

    /**
     * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
     */
    private int mOrientation;

    private final Rect mBounds = new Rect();

    /**
     * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
     * {@link LinearLayoutManager}.
     *
     * @param context Current context, it will be used to access resources.
     * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
     */
    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        if (mDivider == null) {
            Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
                    + "DividerItemDecoration. Please set that attribute all call setDrawable()");
        }
        a.recycle();
        setOrientation(orientation);
    }

    /**
     * Sets the orientation for this divider. This should be called if
     * {@link RecyclerView.LayoutManager} changes orientation.
     *
     * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
     */
    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL && orientation != VERTICAL) {
            throw new IllegalArgumentException(
                    "Invalid orientation. It should be either HORIZONTAL or VERTICAL");
        }
        mOrientation = orientation;
    }

    /**
     * Sets the {@link Drawable} for this divider.
     *
     * @param drawable Drawable that should be used as a divider.
     */
    public void setDrawable(@NonNull Drawable drawable) {
        if (drawable == null) {
            throw new IllegalArgumentException("Drawable cannot be null.");
        }
        mDivider = drawable;
    }

    /**
     * @return the {@link Drawable} for this divider.
     */
    @Nullable
    public Drawable getDrawable() {
        return mDivider;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int top;
        final int bottom;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            top = parent.getPaddingTop();
            bottom = parent.getHeight() - parent.getPaddingBottom();
            canvas.clipRect(parent.getPaddingLeft(), top,
                    parent.getWidth() - parent.getPaddingRight(), bottom);
        } else {
            top = 0;
            bottom = parent.getHeight();
        }

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
            final int right = mBounds.right + Math.round(child.getTranslationX());
            final int left = right - mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

要点:

  • DividerItemDecoration

构造这需要留意一点TypedArray这玩意,自定义属性时我们会碰到常见的两个类AttributeSet与TypedArray。
这两个类都能读取自定义的属性。但二者是有区别的AttributeSet只能在自定义view中使用但时TypedArray比较强大能在任意类中使用。

为啥这里能够直接读取数组呢?这个数组代表啥意思呢?首先我们回顾下自定义view

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--自定义属性:注意这里name填自定义view的名字即可,固定写法-->
    <declare-styleable name="GoogleSignButton">
     <!-- 定义属性名为text,属性值为String类型,根据需求我们还可以定义为其他类型如reference、boolean-->
        <attr name="text" format="string" />
    </declare-styleable>
</resources>
<com.zenni.widgets.GoogleSignButton
    app:text="Google"
    android:id="@+id/socialLogin"
    android:layout_width="279dp"
    android:layout_height="48dp" />

class GoogleSignButton @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet?,
    defStyleAttr: Int = 0
) :
    ConstraintLayout(context, attributeSet, defStyleAttr) {

    val binding: LayoutBtnGoogleSigninBinding by BindViewGroup(R.layout.layout_btn_google_signin)

    init {
      initCustomAttrs(attributeSet)
    }
    // 处理自定义属性
    private fun initCustomAttrs(attributeSet:AttributeSet?) {
        //public final TypedArray obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs)
        val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.GoogleSignButton)
        for (i in 0 until typedArray.indexCount) {
            when (typedArray.getIndex(i)) {
                R.styleable.GoogleSignButton_text -> setText(typedArray.getText(typedArray.getIndex(i)).toString())
            }
        }
        typedArray.recycle()
    }

    fun setText(text: String) {
        binding.tvText.text = text
    }
}

在自定义view获取自定义属性时我们使用到了obtainStyledAttributes,这里的第二个参数就是int类型的数组,

当资源编译后自定义的属性就会被编译成属性数组,如上数组使用R.styleable.GoogleSignButton便可引用到编译后的属性数组,我们自定义的属性就在这个数组中。

这时知道recyclerView默认的DividerItemDecoration中为啥有ATTRS这玩意了吧?其实就是提供读取自定义属性listDivider的值的。

此时我们又有疑问了,上述自定义属性text有值,因为你在xml中设置了,然后你通过TypedArray读取到我懂了,但是DividerItemDecoration中的listDivider这玩意在哪赋值的呢?

系统在 RecyclerView 或 ListView 的默认实现中使用了 listDivider,并将其作为默认的分隔线资源。因此,你在代码中通过 obtainStyledAttributes 获取这个属性时,实际上是在获取当前主题中已经定义好的资源。

再次来个栗子来理解下:通过一个示例来帮助你理解如何使用自定义属性并将本地的一张图片作为 Drawable 获取,类似于 DividerItemDecoration 中使用 ATTRS 的方式。

<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CustomView">
        <attr name="dividerDrawable" format="reference" />
    </declare-styleable>
</resources>

<!-- res/layout/activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- 自定义 View 使用 dividerDrawable 属性 -->
    <com.example.MyCustomView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:dividerDrawable="@drawable/my_local_image" />
        
</LinearLayout>

package com.example;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;

public class MyCustomView extends View {

    private Drawable mDividerDrawable;

    // 自定义属性的 ID 数组
    private static final int[] ATTRS = new int[]{
            R.attr.dividerDrawable
    };

    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);

        // 获取自定义属性的值
        TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
        mDividerDrawable = a.getDrawable(0);
        a.recycle();

        // 现在 mDividerDrawable 就是你在布局文件中指定的图片资源
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        // 在这里你可以使用 mDividerDrawable 进行绘制
        if (mDividerDrawable != null) {
            mDividerDrawable.setBounds(0, 0, getWidth(), mDividerDrawable.getIntrinsicHeight());
            mDividerDrawable.draw(canvas);
        }
    }
}

通过代码获取的就是想要的值,其实设置值在其他地方已经设置好了。

3、Item Animation
4、ItemTouchHelper
5、SnapHelper

LinearLayoutManager的常用方法

canScrollHorizontally();//能否横向滚动
canScrollVertically();//能否纵向滚动
scrollToPosition(int position);//滚动到指定位置

setOrientation(int orientation);//设置滚动的方向
getOrientation();//获取滚动方向

findViewByPosition(int position);//获取指定位置的Item View
findFirstCompletelyVisibleItemPosition();//获取第一个完全可见的Item位置
findFirstVisibleItemPosition();//获取第一个可见Item的位置
findLastCompletelyVisibleItemPosition();//获取最后一个完全可见的Item位置
findLastVisibleItemPosition();//获取最后一个可见Item的位置

1、多类型item

Recycler的多条目的实现其实也是蛮简单的:
1、这里你需要重写getItemViewType方法。
2、然后再onCreateViewHolder中根据不同的viewType加载不同的布局。
3、再onBindViewHolder中根据条目所代表的类型处理相关的逻辑即可。
其实这里有一种优雅的写法,在不破坏我们原来的MyRecyclerAdapter代码的基础下添加新的view类型,那就是使用装饰者设计模式

/**
 * Created by sunnyDay on 2019/9/6 20:02
 * recyclerView的多条目实现
 */
 // 1、装饰者与被装饰着继承或实现相同的对象
public class MyRecyclerAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int TYPE_NORMAL = 0; //普通类型
    private static final int TYPE_HEAD = 1;  // 类型 头
    private static final int TYPE_FOOT = 2; //  类型 尾

    private MyRecyclerAdapter myRecyclerAdapter;// 2、装饰者持有被装饰者引用


    /**
     * 初始化引用
     */
    public MyRecyclerAdapterWrapper(MyRecyclerAdapter myRecyclerAdapter) {
        this.myRecyclerAdapter = myRecyclerAdapter;
    }


    /**
     * @param viewType 代表条目类型
     */
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {

        if (viewType == TYPE_HEAD) {
            return new HeadViewHolder(LayoutInflater.from(myRecyclerAdapter.getContext()).inflate(R.layout.layout_head, viewGroup, false));
        } else if (viewType == TYPE_FOOT) {
            return new FootViewHolder(LayoutInflater.from(myRecyclerAdapter.getContext()).inflate(R.layout.layout_foot, viewGroup, false));
        } else {
            return myRecyclerAdapter.onCreateViewHolder(viewGroup, viewType);
        }

    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {

        if (position == 0 || position == myRecyclerAdapter.getItemCount() + 1) {
            // 索引为 0和最后一个条目时 加载相应的布局
        } else {
            if (viewHolder instanceof MyRecyclerAdapter.MyViewHolder) {
                myRecyclerAdapter.onBindViewHolder((MyRecyclerAdapter.MyViewHolder) viewHolder, position - 1);
            }

        }
    }


    @Override
    public int getItemCount() {
        return myRecyclerAdapter.getItemCount() + 2; // 相当于原来的基础上多添加两个
    }

    /**
     * @param position 条目索引
     * @function 根据条目的索引返回其相应的条目类型。相同类型item返回相同的数值。
     */
    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return TYPE_HEAD;
        } else if (position == myRecyclerAdapter.getItemCount() + 1) {
            return TYPE_FOOT;
        } else
            return TYPE_NORMAL;

    }


    class HeadViewHolder extends RecyclerView.ViewHolder {

        public HeadViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

    class FootViewHolder extends RecyclerView.ViewHolder {

        public FootViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

}

4、添加头尾布局

RecyclerView不像Listview那样提供了添加头尾的方法,需要开发者自己实现。其实搞过上文的多条目后我们添加头尾就很方便啦。

5、数据刷新

数据的更新一般分为两种,在顶部item时我们可以下拉刷新数据。当画到底部当前可见的最后一个item时,再次上拉(上滑)加载数据。

(1)下拉刷新

下拉刷新的逻辑其实还是有点复杂的,我们需要为RecyclerView添加个头布局,这个头布局里面一般包括刷新图标,这个图标还伴有动画,处理之外我们还要重写触摸事件,监听用户的滑动操作。不同的滑动操作对应刷新图标的动画。反正就是属于自定义拓展view的系列了。还好安卓supportv4给我们提供了这个控件帮助我们快速实现。

  <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/pull2refresh"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <android.support.v7.widget.RecyclerView
                android:id="@+id/recycle_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
    </android.support.v4.widget.SwipeRefreshLayout>

    refreshLayout.setOnRefreshListener(this);// 下拉刷新监听
    //刷新时进度条颜色变换,转一圈颜色变化一种
    refreshLayout.setColorSchemeResources(R.color.colorAccent,R.color.colorPrimary);
   /**
     * 下拉刷新回调,模拟数据更新
     */
    @Override
    public void onRefresh() {
        //模拟耗时操作
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                refreshLayout.setRefreshing(false);//取消刷新
            }
        }, 2000);

        mList.add("我是下拉刷新添加的数据");
        mAdapter.notifyDataSetChanged();
        Toast.makeText(this, "刷新数据成功", Toast.LENGTH_SHORT).show();
    }

这种下拉刷新就是典型的圆形进度条转呀转,如果我们想要花里胡哨的操作还是自定义算啦~

(2)上拉加载

数据的上拉加载的实现逻辑相对来说是比较简单的,这里就手动总结下。简单实现思路如下:

1、滑动事件监听
2、判断条目是否是最后一个、可见的条目,且用户正在向上滑。
3、满足2时进行加载数据,更新UI。

        // 滑动监听
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            boolean isSlide2Up = false;

            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);

                LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {//状态为静止没有滑动时
                    if (manager != null) {
                        int lastItemIndex = manager.findLastCompletelyVisibleItemPosition();//获取最后一个可见的条目索引
                        int itemCount = manager.getItemCount();//获取item的数量

                        // 当滑动到最后一个可见条目,且上拉时。加载数据
                        if (lastItemIndex == itemCount - 1 && isSlide2Up) {
                            mList.add("我是上拉加载的数据");
                            mAdapter.notifyDataSetChanged();
                            Toast.makeText(getApplicationContext(), "刷新数据成功", Toast.LENGTH_SHORT).show();
                        }
                    }

                }

            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                // 大于0表示正在向上滑动,小于等于0表示停止或向下滑动
                isSlide2Up = dy > 0;
            }
        });

这里也是简要的实现下逻辑,还是那句话想要花里胡哨那就自定义吧!!!
newState的三种状态:

1、SCROLL_STATE_IDLE :静止没有滚动
2、SCROLL_STATE_DRAGGING :正在被外部拖拽,一般为用户正在用手指滚动
3、SCROLL_STATE_SETTLING :用户画动后,手离开屏幕,条目自动滚动、滑动。

注意:

  • 滑动到最后一个可见条目,且上拉时的判断条件

  • 向上向下滑动的判断(dy,左右时使用dx)

二、RecyclerView的四大组成

1、打造万能的Adapter

RecyclerView的Adapter写多了你就会发现存在着一些重复的工作,这时我们便思考能不能像ListView的adapter一样有个BaseAdapter就好啦~

(1)普通的通用Adapter(适合一种类型的条目)

/**
 * Created by sunnyDay on 2019/9/16 16:36
 * 通用的Adapter封装
 */
public abstract class BaseAdapter<T> extends RecyclerView.Adapter<BaseAdapter.BaseViewHolder> {

    protected Context mContext;
    protected int mLayoutId;
    protected List<T> mData;
    protected LayoutInflater mLayoutInflate;

    public BaseAdapter(Context context, int layoutId, List<T> data) {
        mContext = context;
        mLayoutId = layoutId;
        mData = data;
        mLayoutInflate = LayoutInflater.from(context);
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {

        return BaseViewHolder.getViewHolder(mContext, viewGroup, mLayoutId);

    }

    // 有点小bug(用户使用时,可以重写RecyclerView.Adapter的onBindViewHolder)
    @Override
    public final void onBindViewHolder(@NonNull BaseViewHolder baseViewHolder, int position) {
        convert(baseViewHolder, position);
    }


    public abstract void convert(BaseViewHolder baseViewHolder, int position);


    /**
     * 返回条目个数(进行啦非空处理)
     */
    public int getItemCount() {
        return mData == null ? 0 : mData.size();
    }

    /**
     * 通用的ViewHolder,内部使用SparseArray来缓存View对象
     */
    public static class BaseViewHolder extends RecyclerView.ViewHolder {
        private SparseArray<View> mViews;//键值对为int类型,存储相对于hashMap高效
        private View mConvertView;
        private Context mContext;


        BaseViewHolder(Context context, @NonNull View itemView, ViewGroup parent) {
            super(itemView);
            mContext = context;
            mConvertView = itemView;
            mViews = new SparseArray<>();
        }

        /**
         * 获取ViewHolder
         */
        public static BaseViewHolder getViewHolder(Context context, ViewGroup parent, int layoutId) {
            View view = LayoutInflater.from(context).inflate(layoutId, parent, false);
            return new BaseViewHolder(context, view, parent);
        }


        /**
         * @param viewId view的id
         * @function通过View Id 找到该控件
         */
        @SuppressWarnings("unchecked")
        public <T extends View> T getView(int viewId) {
            View view = mViews.get(viewId);
            if (view == null) {
                view = mConvertView.findViewById(viewId);
                mViews.put(viewId, view);
            }
            return (T) view;
        }
    }
}

简单使用

  mRecyclerView.setAdapter(new BaseAdapter(this,R.layout.layout_recyclerview_item,mList) {
            @Override
            public void convert(BaseAdapter.BaseViewHolder baseViewHolder, int position) {
               AppCompatTextView text = baseViewHolder.getView(R.id.atv_text);
               text.setText("万能适配器:"+position);
            }
        });

直接给个RecyclerView的item布局,再给个数据集合完事。注意这里只重写convert即可不要重写onCreateViewHolder,否则,convert不生效(java的继承机制)

(2)多Item万能Adapter的实现

/**
 * Created by sunnyDay on 2019/9/16 17:48
 */
public abstract class MultiItemBaseAdapter<T> extends BaseAdapter<T> {

    protected MultiItemTypeSupport<T> mMultiItemTypeSupport;

    public MultiItemBaseAdapter(Context context, List<T> data, MultiItemTypeSupport<T> multiItemTypeSupport) {
        super(context, -1, data);
        this.mMultiItemTypeSupport = multiItemTypeSupport;
    }

    @Override
    public int getItemViewType(int position) {
        return mMultiItemTypeSupport.getItemViewType(position, mData.get(position));
    }


    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
        int layoutId = mMultiItemTypeSupport.getLayoutId(viewType);
        return BaseViewHolder.getViewHolder(mContext, viewGroup, layoutId);
    }


/**
 *
 * 我们的ViewHolder是通用的,唯一依赖的就是个layoutId。那么上述第二条就变成,
 * 根据不同的itemView告诉我用哪个layoutId即可,生成viewholder这种事我们通用adapter来做。
 * */
    public interface MultiItemTypeSupport<T> {
        int getLayoutId(int itemType);
        int getItemViewType(int position, T t);
    }
}

3、Item Decoration
4、item Animation

RecyclerView能够通过mRecyclerView.setItemAnimator(ItemAnimator animator)设置添加、删除、移动、改变的动画效果。RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。

(1)提供的默认的动画类及其继承关系图

 mRecyclerView.setItemAnimator(new DefaultItemAnimator());

在这里插入图片描述
(2)类分析

  • ItemAnimator 内部提供了一系列条目变化而触发动画的方法,条目动画的基类。
  • SimpleItemAnimator:实现类,该类提供了一系列更易懂的API,在自定义Item Animator时只需要继承SimpleItemAnimator即可。

SimpleItemAnimator中的重要方法:
animateAdd(ViewHolder holder): 当Item添加时被调用。
animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY): 当Item移动时被调用。
animateRemove(ViewHolder holder): 当Item删除时被调用。
animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop): 当显式调用notifyItemChanged()或notifyDataSetChanged()时被调用。

  • DefaultItemAnimator 继承了SimpleItemAnimator然后实现一堆方法,还是比较麻烦的。

(3)简单方式 实现自定义动画

使用第三方库:recyclerview-animators
而且自定义item动画使用这个也简单多啦,可以玩一些花里胡哨的操作啦。。。

//栗子:

 mRecyclerView.setItemAnimator(new ScaleInAnimator());//使用第三方
 ...
 public void doClick(View view) {
        mList.remove(0);
        mAdapter.notifyItemRemoved(0); // 刷新方法的使用需要留意
    }

三、拓展

1、点击事件、长摁事件

rv相比listview的事件点击、添加头尾还是麻烦点的。事件,这个使用接口回调的方式即可实现,下面就举个点击事件的栗子。。。

public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
    private OnClickListener mOnClickListener;

     //外部设置条目点击事件时回调 view的点击事件
    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, final int position) {
        myViewHolder.text.setText(mList.get(position));
        myViewHolder.layout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mOnClickListener.clicked(v, position);
            }
        });
    }

 public void setOnClickListener(OnClickListener onClickListener) {
        mOnClickListener = onClickListener;
    }

    public interface OnClickListener {
        void clicked(View view, int position);
    }
}
2、结合ItemTouchHelper

安卓系统提供了一个强大的工具类ItemTouchHelper,这个类处理了有关rv的拖拽、侧滑的相关的事情。
简单的使用步骤:
1、实现 ItemTouchHelper.Callback 回调
2、把 ItemTouchHelper 绑定到 RecyclerView 上

/**
 * Created by sunnyDay on 2019/9/20 17:21
 * <p>
 * 侧滑 拖拽的实现
 * 1、实现侧滑删除
 * 2、实现拖动到指定位置
 */
public class MyTouchHelper extends ItemTouchHelper.Callback {

    private MyRecyclerAdapter mRecyclerAdapter;

    public MyTouchHelper(MyRecyclerAdapter recyclerAdapter) {
        mRecyclerAdapter = recyclerAdapter;
    }

    /**
     * 设置支持拖拽滑动的方向(内部通过makeMovementFlags方法设置)
     * 规定条目滑动为:左右
     * 规定条目拖拽为:上下
     */
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        int slideFlag = ItemTouchHelper.START | ItemTouchHelper.END; // 左右滑动
        int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //上下拖拽
        return makeMovementFlags(dragFlag, slideFlag);
    }

    /**
     * 拖拽时回调
     */
    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder viewHolder1) {
        int fromItem = viewHolder.getAdapterPosition();
        int toItem = viewHolder1.getAdapterPosition();

        String prev = mRecyclerAdapter.getmList().remove(fromItem);//删除长摁位置的条目,保存下删除的条目。
        mRecyclerAdapter.getmList().add(toItem > fromItem ? toItem - 1 : toItem, prev);// 吧删除的条目添加到拖动停止的位置
        mRecyclerAdapter.notifyItemMoved(fromItem, toItem);// 通知局部刷新
        return true;
    }

    /**
     * 滑动时回调
     */
    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
        int position = viewHolder.getAdapterPosition();
        mRecyclerAdapter.getmList().remove(position);
        mRecyclerAdapter.notifyItemRemoved(position);
    }

    /**
     * 状态改变时回调
     */
    @Override
    public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
        super.onSelectedChanged(viewHolder, actionState);
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            viewHolder.itemView.setBackgroundColor(Color.parseColor("#ff0000")); //设置拖拽和侧滑时的背景色
        }
    }

    /**
     * 拖拽滑动完成之后回调
     */
    @Override
    public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        viewHolder.itemView.setBackgroundColor(Color.parseColor("#FFFFFF"));
    }

    /**
     * 如果想自定义动画,可以重写这个方法
     * 根据偏移量来设置
     */
    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }

    /**
     * 是否支持长摁拖拽,默认为 true,设置false为关闭。
     */
    @Override
    public boolean isLongPressDragEnabled() {
        return super.isLongPressDragEnabled();
    }
}
        // rv的拖拽侧滑
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new MyTouchHelper(mAdapter));
        itemTouchHelper.attachToRecyclerView(mRecyclerView);
3、结合SnapHelper实现特殊条目滑动效果

SnapHelp 能够辅助 RecyclerView 在滚动结束时将 Item 对齐到某个位置。SnapHelp 是一个抽象类,Android 提供了LinearSnapHelper,可以让 RecyclerView 滚动停止时 Item 停留在中间位置,又提供了 PagerSnapHelper,可以让RecyclerView 像 ViewPager 一样的效果,一次只能滑动一个,并且 Item 居中显示,和 LinearSnapHelper 的区别在于 LinearSnapHelper 支持惯性滑动,所以一次能滑动多个。

        // 支持惯性滑动
        LinearSnapHelper linearSnapHelper = new LinearSnapHelper();
        linearSnapHelper.attachToRecyclerView(mRecyclerView);

        // 类似vp效果
        PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
        pagerSnapHelper.attachToRecyclerView(mRecyclerView);
4、一个用着还行的开源库reclaim
5、一个UI效果

横向一屏幕可见两个item按比例展示

6、嵌套滑动问题和CoordinatorLayout结合时

Android 5.0推出了嵌套滑动机制,在之前,一旦子View处理了触摸事件,父View就没有机会再处理这次的触摸事件,而嵌套滑动机制解决了这个问题.为了支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口,而RecyclerView实现了NestedScrollingChild接口,而CoordinatorLayout实现了NestedScrollingParent接口。

7、RV嵌套RV同向滑动冲突
8、和listview的对比

(1)ListView的相对优点

  • addHeaderView(), addFooterView()添加头视图和尾视图。
  • 通过”android:divider”设置自定义分割线。
  • setOnItemClickListener()和setOnItemLongClickListener()设置点击事件和长按事件。

(2)Rv的优势

  • 默认已经实现了View的复用,不需要类似if(convertView == null)的实现,而且回收机制更加完善。
  • 默认支持局部刷新。
  • 容易实现添加item、删除item的动画效果。
  • 容易实现拖拽、侧滑删除等功能。
  • RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。

小结

把重要知识点过了一遍

End

参考文章:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值