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 自定义ItemDecoration-实现分组吸顶效果
RecyclerView 悬浮吸顶效果实现,支持数据绑定及Touch事件
RecyclerView 悬浮吸顶效果实现,支持数据绑定及Touch事件