美文网首页安卓UIAndroid优秀案例
自定义"RecyclerView"星空列表「

自定义"RecyclerView"星空列表「

作者: 唐_夏影 | 来源:发表于2019-05-17 13:45 被阅读260次

自定义"RecyclerView"列表「多item且互相交错,自定义列表,ViewGroup级」

前段时间看到一个游戏的列表觉得挺不错的,模仿着做个一个类似的,文章的源码,demo用到的设计图「简单版」

因为这个列表对点的精确要求极高,所有我们必须使用屏幕适配方案,本篇文章使用开源库AndroidAutoSize,不清楚的同学可以看这里

整个项目是以360*640为基准进行适配的,,项目源代码已经上传到Github,项目使用kotlin语言,完全不熟悉的可以看这里

进度展示.png

首先,先看需求,这个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}")
            }
        })
    }
}

好了,结束,如果你觉得本文对你有所帮忙,可以资助我继续写下去

相关文章

网友评论

    本文标题:自定义"RecyclerView"星空列表「

    本文链接:https://www.haomeiwen.com/subject/uannaqtx.html