ReyclerView实现悬浮分组功能

主要技术点

  • View.setY(float) 方法的应用
  • View.getTop() 方法计算是否显示及切换悬浮分组

主要功能

  • 基础:实现纵向线性滑动的ReyclcerView
  • 展示分组及内容,两种ViewHolder
  • 在ReyclerView上层,展示一个用于悬浮的ViewHolder
  • 通过监听ReyclerView的滑动,动态切换悬浮ViewHolder的内容

前言

  • 实现RecyclerView悬浮的功能,是使用的Kotlin语言
  • 悬浮功能看似代码简单,其中真的有坑不少,后序有分析
  • 实现悬浮功能,理解了其中的逻辑关系,其实还是很简单的。完全可以举一反三,比如实现view跟着RecycleView在指定情况下滑动等功能。
  • 这篇文章介绍的应该比较简洁,不过有提供代码,欢迎自行获取查看源码

先看看需要展示的数据吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//数据实体类
class Data{
var id:Int = 0
lateinit var context:String
var type: Int = 0//用于指定ViewType 0分组,1内容
var position: Int = 0 // 用于记录当前组,在整个数据中的position
}
//假数据
fun getData(): ArrayList<Data> {
var datas = ArrayList<Data>()
for (i in 0..3) {
for (j in 0..9) {
var data = Data()
data.id = i * 9 + j
if (j == 0) {
data.type = 0
data.context = "我是type=0数据,我是标题:" + (i + 1)//分组数据
} else {
data.type = 1
data.context = "我是type=1数据,我是第" + (i + 1) + "组的数据,内容数据:" + (j + 1)//内容数据
}
datas.add(data)
}
}
return datas
}

两个ViewHolder的实现略

RecycleView适配器简要实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

class MainAdpater : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private var data=ArrayList<Data>()//需要展示的数据源
fun setData(itemdata:ArrayList<Data>){
if (itemdata != null) {
data = itemdata
notifyDataSetChanged()
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {
if(viewType==0){//分组
return Title1ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.title_type1, parent, false))
}else if(viewType==1){//内容
return ContextViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.context, parent, false))
}
return null
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {//绑定数据
if(holder is Title1ViewHolder){
holder.bindDataandListener(position,data.get(position))
}else if(holder is ContextViewHolder){
holder.bindDataandListener(position,data.get(position))
}
}

override fun getItemCount(): Int {
return data.size
}

override fun getItemViewType(position: Int): Int {
return data[position].type
}
}

RecycleView绑定适配器,步骤略

重点

需要展示的xml

  • activity对于xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 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"
    tools:context="top.goluck.recyclerview_2017_9_17.MainActivity">

    <!-- RecyclerView没毛病 -->
    <android.support.v7.widget.RecyclerView
    android:id="@+id/main_reyclerview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />


    <!-- 這是啥?重点,重点,悬浮分组,即:引入了Title1ViewHolder的布局,用于悬浮 -->

    <include
    android:id="@+id/title1"
    layout="@layout/title_type1" />

    </RelativeLayout>
  • Title1ViewHolder的布局xml 即:layout/title_type1,内容可以各种自定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="#999909"
    tools:context="top.goluck.recyclerview_2017_9_17.MainActivity">

    <ImageView
    android:id="@+id/title_type1_img"
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:layout_marginLeft="20dp"
    android:src="@mipmap/ic_launcher"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    <TextView
    android:id="@+id/title_type1_txt"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    android:textColor="#00ff00"
    android:textSize="15sp"
    android:layout_marginLeft="20dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintStart_toEndOf="@+id/title_type1_img"
    app:layout_constraintTop_toTopOf="parent" />

    </android.support.constraint.ConstraintLayout>

activity需要实现的代码 (重中之重,用于切换title分组的逻辑代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

//h1用于保存title分组的实际高度
var h1: Float = 0f

//用于保存当前RecycleView顶部第一个可见的position
var mCurrentPosition: Int = 0

//用于记录,当前悬浮显示的组内容对应的position
var mCurrentTitlePositioin = 0

//给RecycleView添加滑动事件
main_reyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
//title分组的实际高度赋值的地方
h1 = view1.itemView.height.toFloat()
}

override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
//通过第二个可见的position去判断下一个view是否是标题
if (mMainAdpater.getItemViewType(mCurrentPosition + 1) == 0) {
//如果是标题就获取這个view
val view = mLinearLayoutManager.findViewByPosition(mCurrentPosition + 1)
//如果这个view不为null
if (view != null) {
//判断这个view距离顶部的top是否小于h1(标题的高度)
if (view.top <= h1) {
//主要代码:小于等于h1说明,悬浮的view需要往上滑动(h1-view.top)的距离,就可以实现将view1挤上去的效果
view1.itemView.y = -(h1 - view.top)
} else {
//距离不够的情况,判断view1是否有滑动过,如果滑动过就归位。
if (view1.itemView.y != 0f) {
view1.itemView.y = 0f
}
}
}
}
if (mCurrentPosition != mLinearLayoutManager.findFirstVisibleItemPosition()) {
//判断当前记录的可见positon是否一致
mCurrentPosition = mLinearLayoutManager.findFirstVisibleItemPosition()
//不一致就重新赋值
if (view1.itemView.y != 0f) {
//同时判断view1是否有滑动过,如果滑动过就归位。
view1.itemView.y = 0f
}
if (mCurrentPosition < getData().size && mCurrentTitlePositioin != getData()[mCurrentPosition].position) {
//用于更新标题的ViewHolder,即滑动之后更新
mCurrentTitlePositioin = getData()[mCurrentPosition].position
view1.bindDataandListener(mCurrentTitlePositioin, getData()[mCurrentTitlePositioin])
}
}
}
})
if (0 < getData().size && getData().get(0).type == 0) {
//用于更新标题的ViewHolder,即第一次更新
view1.bindDataandListener(0, getData().get(0))
}else{
view1.bindDataandListener(0, getData().get(0))
}

后序

这里需要补充的是,我要回答前言中的问题

  • 嗯,实现悬停简单吧!最重要的代码即上面的 (activity需要实现的代码,就addOnScrollListener事件监听结合 view.getTop()和view.setY()的处理)

  • 其实,这种实现方案是有bug的,重要声明,并不是所有的悬停功能都能这样实现:

    • 如果看了源码,你们就会发现这里的title的高度低于内容view的高度,有问题吗?你可以试试把高度反过来试试!!!

    • 如果你改完高度,运行之后,发现问题了吧?嗯,是酱紫的。

      • mMainAdpater.getItemViewType(mCurrentPosition + 1) == 0,满足这条件的时候,其实title分组的view已经有一部分被悬停的分组view给遮挡了一部分,所以问题就出现了。

      • 一般情况下,标题分组view高度是比内容view的高度高,所以就不会有问题。

      • 这里,问题发现了。就交给大家去解决吧!我觉得不难的,我主要是比较忙,然后目前也还没有遇到这种情况,后续有时间了,应该会考虑这种情况,并完善它的。如果,你有解决方案,欢迎分享交流啊!

嗯,就到这了吧!

本篇完

(注意:转载文章请注明来源[ReyclerView实现悬浮分组功能](http://goluck.top/2017/09/17/ReyclerView%E5%AE%9E%E7%8E%B0%E6%82%AC%E6%B5%AE%E5%88%86%E7%BB%84%E5%8A%9F%E8%83%BD))