实现效果:
实现方案介绍
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