自定义"RecyclerView"列表「多item且互相交错,自定义列表,ViewGroup级」
因为这个列表对点的精确要求极高,所有我们必须使用屏幕适配方案,本篇文章使用开源库AndroidAutoSize,不清楚的同学可以看这里
进度展示.png整个项目是以360*640为基准进行适配的,,项目源代码已经上传到Github,项目使用kotlin语言,完全不熟悉的可以看这里
首先,先看需求,这个item有什么特殊之处
-
item并不是完全一样的,以4个圆圈,也就是4个item为一个“周期”,但是注意,列表并不是只会出现4的倍数,而是有可能为5,6,7这样的数字,这个周期只是一个当item出现以4为倍数时才会出现
1,3,5,7,1,3,5 这样的规律,1表示就是第一个小圆圈,3表示的就大圆圈,5表示不同位置的小圆圈...等等
-
item之间的互相交错的,item与item之间会有重叠的部分,这就决定了我们不能使用recyclerView来做这个列表
-
item与item之间有一条线连着
思路
思路,最外层就是一个FrameLayout帧布局,然后我们的item实际上就只有两个,一个小圆圈item,一个大圆圈item,第1,3,4 个item只是位置变化了而已,其实都还是小圆圈
然后我们再根据返回的列表数据动态的将这些item添加到ViewGroup就可以了,至于item与item之间的连线,我们使用一个Path连接每一个item的中心点,再使用Paint画笔画出来就可以了
另外我们确定点的数值是通过蓝湖提供的标注的,所以注意对比着设计图看代码,Ok,看代码吧
创建实体类
我们需要一个实体类来模拟列表的内容实体类,当点击列表对应item时,会返回点击集合的索引
data class StarryBean(var chapterName: String)
记录item位置和圆点坐标的实体类
/**
* 控制item位置的实体类
* 1,item距离左边的距离
* 2,item距离顶部的距离
* 3,item中心x坐标,也就是圆点的x坐标
* 4,item中心y坐标,也就是圆点的y坐标
*/
data class CircleBean( var marginleft: Int,var marginTop: Int,var dotX: Int, var doty: Int)
创建item
之后我们创建item_cirrcle_bg.xml,也就是大圆圈(切图请前往蓝湖点击下载该页面全部切图,或参考源码)
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<View
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#344268"/>
<RelativeLayout
android:id="@+id/rlContainer"
android:layout_width="255dp"
android:layout_height="255dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<!--进度节点名称-->
<TextView
android:id="@+id/tvChapterName"
android:layout_marginLeft="70dp"
android:layout_marginTop="68dp"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:text="守望"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!--圆圈背景-->
<ImageView
android:src="@drawable/circle"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
小圆圈item和大圆圈一样,只是大小变了,item_cirrcle_small.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!--用来写item时方便看效果的背景-->
<View
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#344268"/>
<RelativeLayout
android:id="@+id/rlContainer"
android:layout_width="138dp"
android:layout_height="138dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<!--进度节点名称-->
<TextView
android:id="@+id/tvChapterName"
android:layout_marginLeft="28dp"
android:layout_marginTop="38dp"
android:textColor="#FFFFFF"
android:textSize="13sp"
android:text="守望"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!--圆圈背景-->
<ImageView
android:src="@drawable/circle"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
View代码
最终控件的代码,另外注意要在初始化时加上setWillNotDraw(false)出行,如果不加上这个属性在ViewGroup级自定义控件时无法绘制Path路径
<color name="color30FFFFFF">#4DFFFFFF</color>
<color name="public_colorWhite">#FFFFFF</color>
需要用到的颜色
/**
* Created by 舍长 on 2019/5/16
* describe:自定义复杂交错的列表
*/
class StarryListView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
//进度实体列表
private var mList = ArrayList<StarryBean>()
//item位置,圆点坐标实体类列表
private var mDistanceList = ArrayList<CircleBean>()
//背景path路径,透明度30%,颜色为白色
private var mBackPath = Path()
//已经完成进度的path路径,透明度100%,白色
private var mCurrentPath = Path()
//背景画笔
private var mBackPaint = Paint()
//已经完成进度的画笔
private var mCurrentPaint = Paint()
//区间组件的间隔
public var mInterval=424f
init {
//初始化4个基准点的距离,具体坐标看蓝湖的设计图,
//注意圆点距离左边标注图给出的是86,但是还要加上圆点的半径,就是91
//这里在一开始就将单位数值为dp的数值转换成px,后面就直接用就可以了
mDistanceList.add(CircleBean(dp2px(22f), dp2px(0f), dp2px(91f), dp2px(69f)))
mDistanceList.add(CircleBean(dp2px(118f), dp2px(31f), dp2px(245f), dp2px(158f)))
mDistanceList.add(CircleBean(dp2px(38f), dp2px(209f), dp2px(107f), dp2px(278f)))
mDistanceList.add(CircleBean(dp2px(118f), dp2px(307f), dp2px(187f), dp2px(376f)))
//初始化背景路径画笔
mBackPaint.style = Paint.Style.STROKE
mBackPaint.strokeWidth = AutoSizeUtils.dp2px(context, 1f).toFloat()//画笔大小
mBackPaint.color = resources.getColor(R.color.color30FFFFFF)//画笔颜色
//初始化实际路径画笔
mCurrentPaint.style = Paint.Style.STROKE
mCurrentPaint.strokeWidth = AutoSizeUtils.dp2px(context, 1f).toFloat()//画笔大小
mCurrentPaint.color = resources.getColor(R.color.public_colorWhite)//画笔颜色
//在ViewGroup容器级自定义控件总,一定要加上这个属性,不然path无法绘制
setWillNotDraw(false)
}
/**
* 设置相应的数据
*/
fun setListData(list: ArrayList<StarryBean>) {
this.mList = list
//路径的起点就是第一个圆点的坐标
mBackPath.moveTo(mDistanceList[0].dotX.toFloat(), mDistanceList[0].doty.toFloat()) //未解锁章节路径
mCurrentPath.moveTo(mDistanceList[0].dotX.toFloat(), mDistanceList[0].doty.toFloat())//已解锁章节路径
/**
* 遍历列表
* 每4个item为一个我们规划好的基础区间,在这个区间上4个item的基础位置是固定的,而到了第二个区间
* 例如4~8 ,9~12,就是分别在对应的区间加上424dp,即时第5个小圆圈顶部距离第一个小圆圈顶部的距离
* 计算区间[1,2(大圆圈),3,4],计算当前区间倍数的个数是当前所以/4(从第二个区间开始计算),例如5/4=1,就是1倍,第二个区间
* 所有基础位置数值要加上1*424dp
* 而判断是具体1,2,3,4哪个基础位置的计算公式为:i - 4 * multiple,
* 例如,第二个大圆圈,例如,5(数列从0开始)-4*1=1「数列从0开始」
*/
for (i in 0 until list.size) {
var multiple = 0//区间倍数
//当数列索引大于3,也就是第二个区间开始时才开始计算倍数
if (i > 3) {
multiple = i / 4
}
//绘制背景线段
mBackPath.lineTo(mDistanceList[i - 4 * multiple].dotX.toFloat(), mDistanceList[i - 4 * multiple].doty.toFloat() + dp2px( mInterval) * multiple)
//如果已经解锁
if(list[i].islock){
mCurrentPath.lineTo(mDistanceList[i - 4 * multiple].dotX.toFloat(), mDistanceList[i - 4 * multiple].doty.toFloat() + dp2px( mInterval) * multiple)
}
//设置布局参数
val params =
LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
//计算item需要距离顶部多少距离,基准距离+倍数乘446
params.topMargin =
mDistanceList[i - 4 * multiple].marginTop + (dp2px(mInterval) * multiple)
//如果当前需要添加的item是大圆圈
if (i == 1 + 4 * multiple) {
val bigView = LayoutInflater.from(context).inflate(R.layout.item_circle_big, null, false)
//设置距离左边的距离
params.leftMargin = mDistanceList[i - 4 * multiple].marginleft
//初始化控件
val rlContainer = bigView.findViewById<RelativeLayout>(R.id.rlContainer)//整个布局
val textView = bigView.findViewById<TextView>(R.id.tvChapterName)//章节名称
//设置点击事件,当点击item时会回调数列的索引
rlContainer.setOnClickListener {
onClickListener.onSelect(i)
}
//设置章节标题
textView.text = list[i].chapterName
//将设置好的item View Add到ViewGroup上
addView(bigView, params)
} else {
//如果当前需要添加的item是小圆圈
val smallView = LayoutInflater.from(context).inflate(R.layout.item_circle_small, null, false)
//设置距离左边的距离
params.leftMargin = mDistanceList[i - 4 * multiple].marginleft
//初始化控件
val rlContainer = smallView.findViewById<RelativeLayout>(R.id.rlContainer)//整个布局
val textView = smallView.findViewById<TextView>(R.id.tvChapterName)//章节名称
//设置点击事件,当点击item时会回调数列的索引
rlContainer.setOnClickListener {
onClickListener.onSelect(i)
}
//设置章节标题
textView.text = list[i].chapterName
//将设置好的item View Add到ViewGroup上
addView(smallView, params)
}
}
invalidate()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawPath(mBackPath, mBackPaint)//绘制为解锁章节背景虚线
canvas?.drawPath(mCurrentPath, mCurrentPaint)//绘制已经解锁章节路径
}
/**
* 用来对选中item位置的回调,onSelect方法也可以根据需要设置成实体类,
* 我是返回集合索引,在Activity中进行处理
*/
private lateinit var onClickListener: OnClickListener
interface OnClickListener {
fun onSelect(index: Int)
}
fun setStarryOnClickListener(onProgressListener: OnClickListener) {
this.onClickListener = onProgressListener
}
}
Activity代码
最后在Activity的xml中使用,要使用ScrollView包住View,不然无法滑动
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.recycler.view.StarryListView
android:background="#344268"
android:id="@+id/starryList"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.example.recycler.view.StarryListView>
</ScrollView>
</android.support.constraint.ConstraintLayout>
在Activity中使用
/**
* Created by 舍长
* describe:
*/
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/**
* 已经解锁的章节的透明度是100%,未解锁章节的透明度是30%
*/
val list = ArrayList<StarryBean>()
list.add(StarryBean("烟火", true))
list.add(StarryBean("守望", true))
list.add(StarryBean("烟火", true))
list.add(StarryBean("守望", true))
list.add(StarryBean("烟火", false))
list.add(StarryBean("守望", false))
list.add(StarryBean("守望", false))
list.add(StarryBean("守望", false))
starryList.setListData(list)
//设置点击回调监听
starryList.setStarryOnClickListener(object :StarryListView.OnClickListener{
override fun onSelect(index: Int) {
toast("点击了${list[index].chapterName}")
}
})
}
}
好了,结束,如果你觉得本文对你有所帮忙,可以资助我继续写下去
网友评论