Android RecyclerView 吸顶效果

Posted danfengw

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android RecyclerView 吸顶效果相关的知识,希望对你有一定的参考价值。

实现效果:

实现方案介绍

recyclerView实现吸顶效果有2种方案:
方案1: 通过ItemDecoration 并重写对应的getItemOffsets()、onDraw()、onDrawOver()方法
方案2: 通过xml布局,并设置RecyclerView的scrollListener的监听的方式

简单讲下2种方式优缺点:
方案1:
优点:封装性好,使用友好,相关逻辑封装在ItemDecoration中,没有外部逻辑
缺点:扩展性不好,需要通过canvas进行绘制,如果吸顶控件ui比较复杂,绘制起来会很麻烦,并且ui一旦不一致就需要重新写canvas相关代码

方案2:
优点:扩展性比较好,吸顶控件可以通过xml的方式进行编写
缺点:封装性不强,需要对RecyclerView设置ScrollListener

最终方案:基于扩展性的考虑,推荐使用方案2,对方案2封装性不强的问题,可以通过代码来封装的更好一些. 关于方案一后面也会有博客写一下的
方案一与方案而,思路上都是比较上一个的标记与下一个的标记是否相同,不相同则更新吸顶布局的ui

方案2 封装与实现

这部分感觉思路上看不太懂的可以先看封装前的代码,封装前代码复制在下方了

lib封装:

BaseStickView

该类主要是为了规范后续其他人是用的时候,创建的HeaderView或者ItemView必须继承BaseStickView—》参考后面Demo中的StickyHeaderView与StickyItemView便能理解


abstract class BaseStickView : FrameLayout 

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)

    init 
        inflate(context, getContentId(), this)
    

    abstract fun onBind(data: Any?)
    abstract fun getContentId(): Int

StickModel

数据模型,对原始数据进行转换,
desTag用于赋值给 itemView.contentDescription,参考 StickHolder中bind方法的代码

class StickModel(
    var itemData: Any? = null,
    var headerData: Any? = null,
    var isHeader: Boolean = false,
    var desTag: String? = null 
)

Holder封装

IStickHolder

interface IStickHolder 
    fun bind(position: Int, data: Any?)
    fun getTag(): Int?

StickHolder

abstract class StickHolder(view: View) : RecyclerView.ViewHolder(view), IStickHolder 

    companion object 
        const val NONE_STICKY_VIEW: Int = 111
        const val FIRST_STICKY_VIEW: Int = 222
        const val HAS_STICKY_VIEW: Int = 333
    

    private var tag: Int? = null
    var data: Any? = null

    override fun bind(position: Int, data: Any?) 
        if (position == 0) 
            tag = FIRST_STICKY_VIEW
         else 
            if (data is StickModel) 
                tag = if (data.isHeader) HAS_STICKY_VIEW else NONE_STICKY_VIEW
            
        
        this.data = data
        if (data is StickModel) 
            itemView.contentDescription = data.desTag
        
    

    override fun getTag(): Int? 
        return tag
    

StickyListLayout

自定义属性attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="StickyListLayout">
        <attr name="stickLayoutId" format="reference|integer"/>
        <attr name="stickHeaderViewId" format="reference|integer"/>
        <attr name="recyclerViewId" format="reference|integer"/>
    </declare-styleable>
</resources>

StickyListLayout代码

class StickyListLayout : FrameLayout 

    private var headerView: BaseStickView? = null
    private var recyclerView: RecyclerView? = null
    private var layoutId: Int = 0
    private var headerViewId: Int = 0
    private var recyclerViewId: Int = 0

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 
        parseAttributes(attrs)
        generateView()
    

    private fun parseAttributes(attrs: AttributeSet?) 
        val ta = context.obtainStyledAttributes(attrs, R.styleable.StickyListLayout)
        layoutId = ta.getResourceId(R.styleable.StickyListLayout_stickLayoutId, 0)
        headerViewId = ta.getResourceId(R.styleable.StickyListLayout_stickHeaderViewId, 0)
        recyclerViewId = ta.getResourceId(R.styleable.StickyListLayout_recyclerViewId, 0)
        ta.recycle()
    

    private fun generateView() 
        if (layoutId == 0 || headerViewId == 0 || recyclerViewId == 0) 
            return
        
        inflate(context, layoutId, this)
        headerView = findViewById(headerViewId)
        recyclerView = findViewById(recyclerViewId)
        initRecyclerView()
    

    private fun initRecyclerView() 
        val lp = recyclerView?.layoutParams
        lp?.width = ViewGroup.LayoutParams.MATCH_PARENT
        lp?.height = ViewGroup.LayoutParams.WRAP_CONTENT
        recyclerView?.layoutManager = LinearLayoutManager(context)
        recyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() 
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) 
                super.onScrolled(recyclerView, dx, dy)
                headerView ?: return
                refreshStickyHeaderView()
                translateStickyHeaderView()
            
        )
    

    private fun translateStickyHeaderView() 
        val underHeaderView = recyclerView?.findChildViewUnder(0f, stickyHeaderView.measuredHeight + 1f)
        underHeaderView ?: return

        val holder = recyclerView?.getChildViewHolder(underHeaderView)
        if (holder is StickHolder) 
            when (holder.getTag()) 
                HAS_STICKY_VIEW -> 
                    val top = underHeaderView.top
                    headerView!!.translationY = if (top > 0) (top - headerView!!.measuredHeight).toFloat() else 0f
                
                else -> 
                    headerView!!.translationY = 0f
                
            
        
    

    private fun refreshStickyHeaderView() 
        val view = recyclerView?.findChildViewUnder(0f, 0f)
        view ?: return
        if (view.contentDescription != headerView!!.contentDescription) 
            val holder = recyclerView?.getChildViewHolder(view)
            if (holder is StickHolder) 
                headerView!!.onBind(holder.data)
            
        
    

    fun bindAdapter(adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>) 
        recyclerView?.adapter = adapter
    

    fun refreshHeaderView(data: Any) 
        stickyHeaderView?.onBind(data)
    

基于封装的使用demo

使用时需要2套Model(HeaderData、ItemData) 2套holder(StickyHeaderHolder、StickyItemHolder) 2套View(StickyHeaderView、StickyItemView),一个Adapter,一个使用Activity

数据

class HeaderData(val iconResId: Int, val title: String)
class ItemData(val iconResId: Int, val des: String)

HeaderView与ItemView 二者都需要继承BaseStickView

headerView

class StickyHeaderView : BaseStickView 

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)

    private val iv: ImageView by lazy 
        this.findViewById<ImageView>(R.id.icon)
    

    private val tv: TextView by lazy 
        this.findViewById<TextView>(R.id.tv)
    
    override fun getContentId(): Int 
        return R.layout.demo_header
    
    
    override fun onBind(data: Any?) 
        if (data !is StickModel) 
            return
        
        val headerData = data.headerData
        headerData ?: return
        if (headerData is HeaderData) 
            iv.setImageResource(headerData.iconResId)
            tv.text = headerData.title
        
    

itemView
class StickyItemView : BaseStickView 

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)

    private val iv: ImageView by lazy 
        this.findViewById<ImageView>(R.id.icon)
    

    private val tv: TextView by lazy 
        this.findViewById<TextView>(R.id.tv)
    


    override fun getContentId(): Int 
        return R.layout.demo_item
    
    
    override fun onBind(data: Any?) 
        if (data !is StickModel) 
            return
        

        val itemData = data.itemData
        itemData ?: return
        if (itemData is ItemData) 
            iv.setImageResource(itemData.iconResId)
            tv.text = itemData.des
        
    


xml文件

demo_header.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#EFFAE7"
        android:gravity="center_vertical"
>
    <ImageView
            android:id="@+id/icon"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_marginLeft="@dimen/dimens_16dp"
            android:layout_marginRight="@dimen/dimens_16dp"
    />
    <TextView
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:gravity="center"
            android:layout_marginLeft="@dimen/dimens_10dp"
            android:layout_toRightOf="@+id/icon"
            android:text="吸顶文本1"/>
</RelativeLayout>

demo_item

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
    <ImageView
            android:id="@+id/icon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginLeft="@dimen/dimens_16dp"
            android:layout_marginRight="@dimen/dimens_16dp"
    />
    <TextView
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textColor="@color/colorPrimaryDark"
            android:layout_toRightOf="@+id/icon"
            android:layout_centerVertical="true"
    />
    <View
            android:layout_width="match_parent"
            android:layout_height="2dp"
            android:layout_alignParentBottom="true"
            android:background="#ffffff"
            android:layout_below="@+id/icon"
    />
</RelativeLayout>

DemoAdapter

class DemoAdapter(var data: MutableList<StickModel>? = null) : RecyclerView.Adapter<StickHolder>() 
    companion object 
        const val ITEM_TYPE_HEADER: Int = 0x11
        const val ITEM_TYPE_ITEM: Int = 0x22
    

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StickHolder 
        return when (viewType) 
            ITEM_TYPE_HEADER -> StickHeaderHolder(parent)
            else -> StickItemHolder(parent)
        
    

    override fun getItemViewType(position: Int): Int 
        return if (data?.get(position)?.isHeader == true) ITEM_TYPE_HEADER else ITEM_TYPE_ITEM
    

    override fun getItemCount(): Int 
        return data?.size ?: 0
    
    
    override fun onBindViewHolder(holder: StickHolder, position: Int) 
        data?.get(position) ?: return
        holder.bind(position, data?.get(position))
    

DemoActivity

xml

activity_demo.xml
StickyListLayout必须配置对应的app:stickLayoutId、app:stickLayoutId、app:recyclerViewId
这几项

<?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"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <com.example.stickyheaderlistdemo.sample.view.StickyListLayout
            android:id="@+id/stickyListLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:stickLayoutId="@layout/sticky_layout"
            app:stickLayoutId="@id/stickyHeaderView"
            app:recyclerViewId="@id/recyclerView"
    />
</LinearLayout>

sticky_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
        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"
        android:background="@color/colorAccent">
    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"/>

    <com.example.stickyheaderlistdemo.sample.demo.view.StickyHeaderView
            android:id="@+id/stickyHeaderView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

</FrameLayout>

demoActivity

class DemoActivity : AppCompatActivity() 
    private var stickModels: MutableList<StickModel> = mutableListOf()
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo)
        initData()
        stickyListLayout.bindAdapter(DemoAdapter(stickModels) as RecyclerView.Adapter<RecyclerView.ViewHolder>)
        stickyListLayout.refreshHeaderView(stickModels!![0])
    

    private fun initData() 
        stickModels.clear()
        for (index in 0..99) 
            if (index < 15) 
                if (index == 0) 
                    stickModels?.add(StickModel(null, HeaderData(R.drawable.surprise, "这组是惊喜的表情"), true, "surprise"))
                
                stickModels?.add(
                    StickModel(
                        ItemData(R.drawable.surprise, "第$index个惊喜的表情 hhhhhhhhhhhhhh"),
                        null,
                        false,
                        "surprise"
                    )
                )
             else if (index < 25) 
                if (index == 15) 
                    stickModels?.add(StickModel(null, HeaderData(R.drawable.angry, "这组是生气的表情"), true, "angry"))
                
                stickModels?.add(StickModel(ItemData(R.drawable.angry, "第$index个生气的表情"), null, false, "angry"))
             else if (index < 35) 
                if (index == 25) 
                    stickModels?.add(StickModel(null, HeaderData(R.drawable.sad, "这组是悲伤的表情"), true, "sad"))
                
                stickModels?.add(StickModel(ItemData(R.drawable.sad, "第$index个悲伤的表情"), null, false, "sad"))
             else 
                if (index == 35) 
                    stickModels?.add(StickModel(null, HeaderData(R.drawable.happy, "这一组是高兴的表情"), true, "happy"))
                
                stickModels?.add(StickModel(ItemData(R.drawable.happy, "第$index个高兴的表情"), null, false, "happy"))
            
        

    

封装前的代码

封装前的代码是参考的其他人的,这里直接贴出来

Activity相关

xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/cy"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"/>
    <TextView
            android:id="@+id/tv_sticky_header_view"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="#EFFAE7"
            android:gravity="center"
            android:text="吸顶文本1"/>

</FrameLayout>

SampleActivity

class SampleActivity : AppCompatActivity() 

    private var recyclerView: RecyclerView? = null
    private var tvStickyHeaderView: TextView? = null
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sample_1)
        initView()
        initListener()
    

    private fun initListener() 
        recyclerView?.addOnScrollListener(object : OnScrollListener() 
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) 
                super.onScrolled(recyclerView, dx, dy)
                tvStickyHeaderView ?: return
                updateStickyHeader(recyclerView)

                val transInfoView = recyclerView.findChildViewUnder(0f, (tvStickyHeaderView?.height ?: 0 + 1).toFloat())
                transInfoView?.tag?.let 
                    val transViewStatus: Int =it as Int
                    val top = transInfoView.top

                    when (transViewStatus) 
                        HAS_STICKY_VIEW -> 
                            tvStickyHeaderView?.translationY = if (top > 0) (top - tvStickyHeaderView!!.measuredHeight).toFloat() else 0f
                        
                        else -> 
                            tvStickyHeaderView?.translationY = 0f
                        
                    
                
            
        )
    

    private fun updateStickyHeader(recyclerView: RecyclerView) 
        val stickView = recyclerView.findChildViewUnder(0f, 0f)
        stickView?.contentDescription ?: return
        if (stickView.contentDescription != tvStickyHeaderView?.text) 
            tvStickyHeaderView?.text = stickView.contentDescription
        
    

    private fun initView() 
        recyclerView = findViewById(R.id.recyclerView)
        tvStickyHeaderView = findViewById(R.id.tv_sticky_header_view)
        recyclerView?.layoutManager = LinearLayoutManager(this)
        recyclerView?.adapter = SampleAdapter2(getData())
    

    private fun getData(): MutableList<StickyBean>? 
        val stickyExampleModels = mutableListOf<StickyBean>()

        for (index in 0..99) 
            if (index < 15) 
                if (index == 0) 
                    stickyExampleModels.add(StickyBean("吸顶文本1", "name$index", "gender$index", true))
                
                stickyExampleModels.add(StickyBean("吸顶文本1", "name$index", "gender$index", false))
             else if (index < 25) 
                if (index == 15) 
                    stickyExampleModels.add(StickyBean("吸顶文本2", "name$index", "gender$index", true))
                
                stickyExampleModels.add(StickyBean("吸顶文本2", "name$index", "gender$index", false))
             else if (index < 35) 
                if (index == 25) 
                    stickyExampleModels.add(StickyBean("吸顶文本3", "name$index", "gender$index", true))
                
                stickyExampleModels.add(StickyBean("吸顶文本3", "name$index", "gender$index", false))
             else 
                if (index == 35) 
                    stickyExampleModels.add(StickyBean("吸顶文本4", "name$index", "gender$index", true))
                
                stickyExampleModels.add(StickyBean("吸顶文本4", "name$index", "gender$index", false))
            
        
        return stickyExampleModels
    

bean

public class StickyBean 
 
  public String name;
  public String autor;
  public String sticky;
  public Boolean isHeader;

  public StickyBean(String sticky,String name,String autor,Boolean isHeader) 
    this.sticky = sticky;
    this.name = name;
    this.autor = autor;
    this.isHeader = isHeader;
  

Adapter

class SampleAdapter2(var data: MutableList<StickyBean>? = mutableListOf()) :
    RecyclerView.Adapter<Holder>() 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder 
        return Holder(LayoutInflater.from(parent.context).inflate(R.layout.sample_1_item, parent, false))
    

    override fun getItemCount(): Int 
        return data?.size ?: 0
    

    override fun onBindViewHolder(holder: Holder, position: Int) 
        holder.bindData(data?.get(position), position, data)
    



class Holder(view: View) : RecyclerView.ViewHolder(view) 
    var tvStickyHeader: TextView? = null
    var rlContentWrapper: RelativeLayout? = null
    var tvName: TextView? = null
    var tvGender: TextView? = null

    companion object 
        const val NONE_STICKY_VIEW: Int = 111
        const val FIRST_STICKY_VIEW: Int = 222
        const val HAS_STICKY_VIEW: Int = 333
    


    init 
        tvStickyHeader = view.findViewById(R.id.tv_sticky_header_view)
        rlContentWrapper = view.findViewById(R.id.rl_content_wrapper)
        tvName = view.findViewById(R.id.name)
        tvGender = view.findViewById(R.id.auto)
    

    fun bindData(stickyBean: StickyBean?, position: Int, data: MutableList<StickyBean>?) 
        stickyBean ?: return
        tvName?.text = stickyBean.name
        tvGender?.text = stickyBean.autor
        if (position == 0) 
            tvStickyHeader?.visibility = View.VISIBLE
            tvStickyHeader?.text = stickyBean.sticky
            itemView.tag = FIRST_STICKY_VIEW
         else 
            if (stickyBean.isHeader) 
                tvStickyHeader?.visibility = View.VISIBLE
                tvStickyHeader?.text = stickyBean.sticky
                itemView.tag = HAS_STICKY_VIEW
             else 
                tvStickyHeader?.visibility = View.GONE
                tvStickyHeader?.text = stickyBean.sticky
                itemView.tag = NONE_STICKY_VIEW
            
        
        itemView.contentDescription = stickyBean.sticky
    

xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:id="@+id/rl_content_wrapper"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp">

        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true" />

        <TextView
            android:id="@+id/auto"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true" />

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_alignParentBottom="true"
            android:background="#ffffff" />

    </RelativeLayout>

    <TextView
        android:id="@+id/tv_sticky_header_view"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#EFFAE7"
        android:gravity="center"
        android:text="吸顶文本1" />

</FrameLayout>

以上是关于Android RecyclerView 吸顶效果的主要内容,如果未能解决你的问题,请参考以下文章

Android 最流行的吸顶效果的实现及代码

Android 自定义ItemDecoration-实现分组吸顶效果

RecyclerView 悬浮吸顶效果实现,支持数据绑定及Touch事件

RecyclerView 悬浮吸顶效果实现,支持数据绑定及Touch事件

RecyclerView 悬浮吸顶效果实现,支持数据绑定及Touch事件

RecyclerView高级用法:分组+吸顶效果实现