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>

转载:https://blog.51cto.com/u_15757710/5593715

猜你喜欢

转载自blog.csdn.net/gqg_guan/article/details/132192914