安卓 车轮视图 WheelView kotlin

安卓 车轮视图 WheelView kotlin

  • 前言
  • 一、代码解析
    • 1.初始化
    • 2.初始化数据
    • 3.onMeasure
    • 4.onDraw
    • 5.onTouchEvent
    • 6.其他
  • 6.ItemObject
  • 二、完整代码
  • 总结


在这里插入图片描述

前言

有个需求涉及到类似这个视图,于是在网上找了个轮子,自己改吧改吧用,拿来主义当然后,但做事不仅要知其然,还要知其所以然,所以拿来用的同时还要理解。于是就有了本文。


一、代码解析

参数

    /**
     * 控件宽度
     */
    private var controlWidth = 0f

    /**
     * 控件高度
     */
    private var controlHeight = 0f
    /**
     * 是否正在滑动
     *
     * @return
     */
    /**
     * 是否滑动中
     */
    var isScrolling = false


    /**
     * 选择的内容
     */
    private val itemList: MutableList<ItemObject>? = mutableListOf()

    /**
     * 设置数据
     */
    private var dataList = mutableListOf<String>()

    /**
     * 按下的坐标
     */
    private var downY = 0

    /**
     * 按下的时间
     */
    private var downTime: Long = 0

    /**
     * 短促移动
     */
    private val goonTime: Long = 200

    /**
     * 短促移动距离
     */
    private val goonDistance = 100

    /**
     * 画线画笔
     */
    private var linePaint: Paint? = null

    /**
     * 线的默认颜色
     */
    private var lineColor = -0x1000000

    /**
     * 线的默认宽度
     */
    private var lineHeight = 2f

    /**
     * 默认字体
     */
    private var normalFont = 14.0f

    /**
     * 选中的时候字体
     */
    private var selectedFont = 22.0f

    /**
     * 单元格高度
     */
    private var unitHeight = 50

    /**
     * 显示多少个内容
     */
    private var itemNumber = 7

    /**
     * 默认字体颜色
     */
    private var normalColor = -0x1000000

    /**
     * 选中时候的字体颜色
     */
    private var selectedColor = -0x10000

    /**
     * 蒙板高度
     */
    private var maskHeight = 48.0f

    /**
     * 选择监听
     */
    private var onSelectListener: OnSelectListener? = null
    /**
     * 设置是否可用
     *
     * @param isEnable
     */
    var isEnable = true

    /**
     * 是否允许选空
     */
    private var noEmpty = true

    /**
     * 正在修改数据,避免ConcurrentModificationException异常
     */
    private var isClearing = false

1.初始化

    /**
     * 初始化,获取设置的属性
     *
     * @param context
     * @param attrs
     */
    private fun init(context: Context, attrs: AttributeSet?) {
        val attribute = context.obtainStyledAttributes(attrs, R.styleable.WheelView)
        unitHeight =
            attribute.getDimension(R.styleable.WheelView_unitHeight, unitHeight.toFloat()).toInt()
        itemNumber = attribute.getInt(R.styleable.WheelView_itemNumber, itemNumber)
        normalFont = attribute.getDimension(R.styleable.WheelView_normalTextSize, normalFont)
        selectedFont = attribute.getDimension(R.styleable.WheelView_selectedTextSize, selectedFont)
        normalColor = attribute.getColor(R.styleable.WheelView_normalTextColor, normalColor)
        selectedColor = attribute.getColor(R.styleable.WheelView_selectedTextColor, selectedColor)
        lineColor = attribute.getColor(R.styleable.WheelView_lineColor, lineColor)
        lineHeight = attribute.getDimension(R.styleable.WheelView_lineHeight, lineHeight)
        maskHeight = attribute.getDimension(R.styleable.WheelView_maskHeight, maskHeight)
        noEmpty = attribute.getBoolean(R.styleable.WheelView_noEmpty, true)
        isEnable = attribute.getBoolean(R.styleable.WheelView_isEnable, true)
        attribute.recycle()
        controlHeight = (itemNumber * unitHeight).toFloat()
    }

上面的代码在构造函数中调用,通过 context.obtainStyledAttributes(attrs, R.styleable.WheelView) 方法拿到我们在attrs.xml文件中自定义样式的一些参数。这些参数是可以在xml中配置的。这些都是自定义view的一些基础,属于老生常谈了。

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="WheelView">
        <attr name="unitHeight" format="dimension" />
        <attr name="itemNumber" format="integer"/>

        <attr name="normalTextColor" format="color" />
        <attr name="normalTextSize" format="dimension" />
        <attr name="selectedTextColor" format="color" />
        <attr name="selectedTextSize" format="dimension" />

        <attr name="lineColor" format="color" />
        <attr name="lineHeight" format="dimension" />

        <attr name="maskHeight" format="dimension"/>
        <attr name="noEmpty" format="boolean"/>
        <attr name="isEnable" format="boolean"/>
    </declare-styleable>

</resources>

2.初始化数据

    /**
     * 初始化数据
     */
    private fun initData() {
        isClearing = true
        itemList!!.clear()
        for (i in dataList.indices) {
            val itemObject = ItemObject()
            itemObject.id = i
            itemObject.itemText = dataList[i]
            itemObject.x = 0
            itemObject.y = i * unitHeight
            itemList.add(itemObject)
        }
        isClearing = false
    }

这里就是初始化item数据,ItemObject是单个数据的绘制,后面再说。而isClearing 是为了避免 ConcurrentModificationException,在drawList()方法中有体现。

    @Synchronized
    private fun drawList(canvas: Canvas) {
        if (isClearing) return
        try {
            for (itemObject in itemList!!) {
                itemObject.drawSelf(canvas, measuredWidth)
            }
        } catch (e: Exception) {
            Log.e("WheelView", "drawList: $e")
        }
    }

3.onMeasure

自定义view的三件套之一

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        controlWidth = measuredWidth.toFloat()
        if (controlWidth != 0f) {
            setMeasuredDimension(measuredWidth, itemNumber * unitHeight)
        }
    }

先用measuredWidthcontrolWidth 赋值 ,然后当宽度不为0的时候调用setMeasuredDimension方法给具体的测量值。我们来看看setMeasuredDimension方法

在这里插入图片描述
这是一个view的自带方法,onMeasure(int,int)必须调用此方法来存储测量的宽度和测量的高度。否则将在测量时触发异常。
参数:

  • measuredWidth–此视图的测量宽度。可以是MEASURED_SIZE_mask和MEASURED_STATE_TOO_SMALL定义的复杂位掩码。
  • measuredHeight–此视图的测量高度。可以是MEASURED_SIZE_mask和MEASURED_STATE_TOO_SMALL定义的复杂位掩码。

4.onDraw

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawLine(canvas)
        drawList(canvas)
        drawMask(canvas)
    }

在其中绘制了三个东西,一个是绘制选中的两个线条

    /**
     * 绘制线条
     *
     * @param canvas
     */
    private fun drawLine(canvas: Canvas) {
        if (linePaint == null) {
            linePaint = Paint()
            linePaint!!.color = lineColor
            linePaint!!.isAntiAlias = true
            linePaint!!.strokeWidth = lineHeight
        }

        canvas.drawLine(
            0f, controlHeight / 2 - unitHeight / 2 + lineHeight,
            controlWidth, controlHeight / 2 - unitHeight / 2 + lineHeight, linePaint!!
        )
        canvas.drawLine(
            0f, controlHeight / 2 + unitHeight / 2 - lineHeight,
            controlWidth, controlHeight / 2 + unitHeight / 2 - lineHeight, linePaint!!
        )
    }

一个是绘制列表,上面已经说过了,还有一个就是绘制蒙层,我这边是有一个渐变的蒙层,也是我做过改动的地方

    /**
     * 绘制遮盖板
     *
     * @param canvas
     */

    private fun drawMask(canvas: Canvas) {
        val colorArray = intArrayOf(0x0042C8FF, 0x3D42C8FF, 0x0042C8FF)
        val positionArray = floatArrayOf(0f, 0.5f, 1f)
        val lg3 = LinearGradient(
            0f, 0f,
            controlWidth, 0f, colorArray, positionArray, TileMode.MIRROR
        )
        val paint3 = Paint()
        paint3.shader = lg3
        canvas.drawRect(
            0f, controlHeight / 2 - unitHeight / 2 + lineHeight, controlWidth,
            controlHeight / 2 + unitHeight / 2 - lineHeight, paint3
        )
    }

5.onTouchEvent

触摸事件

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (!isEnable) return true
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                isScrolling = true
                downY = event.y.toInt()
                downTime = System.currentTimeMillis()
            }

            MotionEvent.ACTION_MOVE -> {
                actionMove(y - downY)
                onSelectListener()
            }

            MotionEvent.ACTION_UP -> {
                val move = Math.abs(y - downY)
                // 判断这段时间移动的距离
                if (System.currentTimeMillis() - downTime < goonTime && move > goonDistance) {
                    goonMove(y - downY)
                } else {
                    actionUp(y - downY)
                    noEmpty()
                    isScrolling = false
                }
            }

            else -> {}
        }
        return true
    }

代码解析:
isEnable是控制是否能滑动的,不用过多关注
在手势为ACTION_DOWN 的时候,记录开始滑动的y坐标和时间,在手势为**ACTION_MOVE **的时候开始移动,并调用actionMove方法设置移动的坐标,然后调用invalidate方法进行重绘。onSelectListener是一个滑动时候的选中监听

    /**
     * 移动的时候
     *
     * @param move
     */
    private fun actionMove(move: Int) {
        for (item in itemList!!) {
            item.move(move)
        }
        invalidate()
    }

最后在手势为ACTION_UP 的时候,判断在ACTION_DOWN这段时间移动的距离,如果当前移动的时间小于短促移动的时间,当前移动的距离却大于短促移动的距离,那么我们就调用goonMove方法多移动一点距离以达到一个惯性移动的体验。

    /**
     * 继续移动一定距离
     */
    @Synchronized
    private fun goonMove(move: Int) {
        Thread {
            var distance = 0
            while (distance < unitHeight * MOVE_NUMBER) {
                try {
                    Thread.sleep(5)
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
                actionThreadMove(if (move > 0) distance else distance * -1)
                distance += 10
            }
            actionUp(if (move > 0) distance - 10 else distance * -1 + 10)
            noEmpty()
        }.start()
    }
    
    /**
     * 移动,线程中调用
     *
     * @param move
     */
    private fun actionThreadMove(move: Int) {
        for (item in itemList!!) {
            item.move(move)
        }
        val rMessage = Message()
        rMessage.what = REFRESH_VIEW
        mHandler.sendMessage(rMessage)
    }

否则就直接用actionUp和noEmpty直接移动

    /**
     * 松开的时候
     *
     * @param move
     */
    private fun actionUp(move: Int) {
        var newMove = 0
        if (move > 0) {
            for (i in itemList!!.indices) {
                if (itemList[i].isSelected) {
                    newMove = itemList[i].moveToSelected().toInt()
                    if (onSelectListener != null) onSelectListener!!.endSelect(
                        itemList[i].id,
                        itemList[i].itemText
                    )
                    break
                }
            }
        } else {
            for (i in itemList!!.indices.reversed()) {
                if (itemList[i].isSelected) {
                    newMove = itemList[i].moveToSelected().toInt()
                    if (onSelectListener != null) onSelectListener!!.endSelect(
                        itemList[i].id,
                        itemList[i].itemText
                    )
                    break
                }
            }
        }
        for (item in itemList) {
            item.newY(move + 0)
        }
        slowMove(newMove)
        val rMessage = Message()
        rMessage.what = REFRESH_VIEW
        mHandler.sendMessage(rMessage)
    }

    /**
     * 不能为空,必须有选项
     */
    private fun noEmpty() {
        if (!noEmpty) return
        for (item in itemList!!) {
            if (item.isSelected) return
        }
        val move = itemList[0].moveToSelected().toInt()
        if (move < 0) {
            defaultMove(move)
        } else {
            defaultMove(
                itemList[itemList.size - 1]
                    .moveToSelected().toInt()
            )
        }
        for (item in itemList) {
            if (item.isSelected) {
                if (onSelectListener != null) onSelectListener!!.endSelect(item.id, item.itemText)
                break
            }
        }
    }

6.其他

    /**
     * 缓慢移动
     *
     * @param move
     */
    @Synchronized
    private fun slowMove(move: Int) {
        Thread { // 判断正负
            var m = if (move > 0) move else move * -1
            val i = if (move > 0) 1 else -1
            // 移动速度
            val speed = 1
            while (true) {
                m -= speed
                if (m <= 0) {
                    for (item in itemList!!) {
                        item.newY(m * i)
                    }
                    val rMessage = Message()
                    rMessage.what = REFRESH_VIEW
                    mHandler.sendMessage(rMessage)
                    try {
                        Thread.sleep(2)
                    } catch (e: InterruptedException) {
                        e.printStackTrace()
                    }
                    break
                }
                for (item in itemList!!) {
                    item.newY(speed * i)
                }
                val rMessage = Message()
                rMessage.what = REFRESH_VIEW
                mHandler.sendMessage(rMessage)
                try {
                    Thread.sleep(2)
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
            }
            if (itemList != null) {
                for (item in itemList) {
                    if (item.isSelected) {
                        if (onSelectListener != null) onSelectListener!!.endSelect(
                            item.id,
                            item.itemText
                        )
                        break
                    }
                }
            }
        }.start()
    }

    /**
     * 移动到默认位置
     *
     * @param move
     */
    private fun defaultMove(move: Int) {
        for (item in itemList!!) {
            item.newY(move)
        }
        val rMessage = Message()
        rMessage.what = REFRESH_VIEW
        mHandler.sendMessage(rMessage)
    }

一些移动相关的方法。


6.ItemObject

单个绘制里面值得说的就是drawSelf方法了,注释都写的比较清晰了。

  /**
         * 绘制自身
         *
         * @param canvas         画板
         * @param containerWidth 容器宽度
         */
        fun drawSelf(canvas: Canvas, containerWidth: Int) {
            if (textPaint == null) {
                textPaint = TextPaint()
                textPaint!!.isAntiAlias = true
            }
            if (textRect == null) textRect = Rect()

            // 判断是否被选择
            if (isSelected) {
                textPaint!!.color = selectedColor
                // 获取距离标准位置的距离
                var moveToSelect = moveToSelected()
                moveToSelect = if (moveToSelect > 0) moveToSelect else moveToSelect * -1
                // 计算当前字体大小
                val textSize =
                    normalFont + (selectedFont - normalFont) * (1.0f - moveToSelect / unitHeight.toFloat())
                textPaint!!.textSize = textSize
            } else {
                textPaint!!.color = normalColor
                textPaint!!.textSize = normalFont
            }


            // 判断是否可视
            if (!isInView) return

            //判断是一行还是两行,两行数据用,分割
            if (itemText.indexOf(",") != -1) {
                var (text1, text2) = itemText.split(",")


                // 返回包围整个字符串的最小的一个Rect区域
                text1 = TextUtils.ellipsize(
                    text1,
                    textPaint,
                    containerWidth.toFloat(),
                    TextUtils.TruncateAt.END
                ) as String
                textPaint!!.getTextBounds(text1, 0, text1.length, textRect)
                //双排文字一
                canvas.drawText(
                    text1,
                    x + controlWidth / 2 - textRect!!.width() / 2,
                    (y + move + unitHeight / 5 * 2 + textRect!!.height() / 5 * 2).toFloat(),
                    textPaint!!
                )
                // 返回包围整个字符串的最小的一个Rect区域
                text2 = TextUtils.ellipsize(
                    text2,
                    textPaint,
                    containerWidth.toFloat(),
                    TextUtils.TruncateAt.END
                ) as String
                textPaint!!.getTextBounds(text2, 0, text2.length, textRect)
                //双排文字2
                canvas.drawText(
                    text2,
                    x + controlWidth / 2 - textRect!!.width() / 2,
                    (y + move + unitHeight / 5 * 3 + textRect!!.height() / 5 * 3).toFloat(),
                    textPaint!!
                )
            } else {
                // 返回包围整个字符串的最小的一个Rect区域
                itemText = TextUtils.ellipsize(
                    itemText,
                    textPaint,
                    containerWidth.toFloat(),
                    TextUtils.TruncateAt.END
                ) as String
                textPaint!!.getTextBounds(itemText, 0, itemText.length, textRect)
                // 绘制内容
                canvas.drawText(
                    itemText, x + controlWidth / 2 - textRect!!.width() / 2,
                    (y + move + unitHeight / 2 + textRect!!.height() / 2).toFloat(), textPaint!!
                )
            }


        }

二、完整代码




import android.content.Context
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Shader.TileMode
import android.os.Handler
import android.os.Message
import android.text.TextPaint
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View



class WheelView : View {
    /**
     * 控件宽度
     */
    private var controlWidth = 0f

    /**
     * 控件高度
     */
    private var controlHeight = 0f
    /**
     * 是否正在滑动
     *
     * @return
     */
    /**
     * 是否滑动中
     */
    var isScrolling = false


    /**
     * 选择的内容
     */
    private val itemList: MutableList<ItemObject>? = mutableListOf()

    /**
     * 设置数据
     */
    private var dataList = mutableListOf<String>()

    /**
     * 按下的坐标
     */
    private var downY = 0

    /**
     * 按下的时间
     */
    private var downTime: Long = 0

    /**
     * 短促移动
     */
    private val goonTime: Long = 200

    /**
     * 短促移动距离
     */
    private val goonDistance = 100

    /**
     * 画线画笔
     */
    private var linePaint: Paint? = null

    /**
     * 线的默认颜色
     */
    private var lineColor = -0x1000000

    /**
     * 线的默认宽度
     */
    private var lineHeight = 2f

    /**
     * 默认字体
     */
    private var normalFont = 14.0f

    /**
     * 选中的时候字体
     */
    private var selectedFont = 22.0f

    /**
     * 单元格高度
     */
    private var unitHeight = 50

    /**
     * 显示多少个内容
     */
    private var itemNumber = 7

    /**
     * 默认字体颜色
     */
    private var normalColor = -0x1000000

    /**
     * 选中时候的字体颜色
     */
    private var selectedColor = -0x10000

    /**
     * 蒙板高度
     */
    private var maskHeight = 48.0f

    /**
     * 选择监听
     */
    private var onSelectListener: OnSelectListener? = null
    /**
     * 设置是否可用
     *
     * @param isEnable
     */
    var isEnable = true

    /**
     * 是否允许选空
     */
    private var noEmpty = true

    /**
     * 正在修改数据,避免ConcurrentModificationException异常
     */
    private var isClearing = false

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    ) {
        init(context, attrs)
        initData()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(context, attrs)
        initData()
    }

    constructor(context: Context?) : super(context) {
        initData()
    }

    /**
     * 初始化,获取设置的属性
     *
     * @param context
     * @param attrs
     */
    private fun init(context: Context, attrs: AttributeSet?) {
        val attribute = context.obtainStyledAttributes(attrs, R.styleable.WheelView)
        unitHeight =
            attribute.getDimension(R.styleable.WheelView_unitHeight, unitHeight.toFloat()).toInt()
        itemNumber = attribute.getInt(R.styleable.WheelView_itemNumber, itemNumber)
        normalFont = attribute.getDimension(R.styleable.WheelView_normalTextSize, normalFont)
        selectedFont = attribute.getDimension(R.styleable.WheelView_selectedTextSize, selectedFont)
        normalColor = attribute.getColor(R.styleable.WheelView_normalTextColor, normalColor)
        selectedColor = attribute.getColor(R.styleable.WheelView_selectedTextColor, selectedColor)
        lineColor = attribute.getColor(R.styleable.WheelView_lineColor, lineColor)
        lineHeight = attribute.getDimension(R.styleable.WheelView_lineHeight, lineHeight)
        maskHeight = attribute.getDimension(R.styleable.WheelView_maskHeight, maskHeight)
        noEmpty = attribute.getBoolean(R.styleable.WheelView_noEmpty, true)
        isEnable = attribute.getBoolean(R.styleable.WheelView_isEnable, true)
        attribute.recycle()
        controlHeight = (itemNumber * unitHeight).toFloat()
    }

    /**
     * 初始化数据
     */
    private fun initData() {
        isClearing = true
        itemList!!.clear()
        for (i in dataList.indices) {
            val itemObject = ItemObject()
            itemObject.id = i
            itemObject.itemText = dataList[i]
            itemObject.x = 0
            itemObject.y = i * unitHeight
            itemList.add(itemObject)
        }
        isClearing = false
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        controlWidth = measuredWidth.toFloat()
        if (controlWidth != 0f) {
            setMeasuredDimension(measuredWidth, itemNumber * unitHeight)
        }
    }


    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawLine(canvas)
        drawList(canvas)
        drawMask(canvas)
    }

    /**
     * 绘制线条
     *
     * @param canvas
     */
    private fun drawLine(canvas: Canvas) {
        if (linePaint == null) {
            linePaint = Paint()
            linePaint!!.color = lineColor
            linePaint!!.isAntiAlias = true
            linePaint!!.strokeWidth = lineHeight
        }

        canvas.drawLine(
            0f, controlHeight / 2 - unitHeight / 2 + lineHeight,
            controlWidth, controlHeight / 2 - unitHeight / 2 + lineHeight, linePaint!!
        )
        canvas.drawLine(
            0f, controlHeight / 2 + unitHeight / 2 - lineHeight,
            controlWidth, controlHeight / 2 + unitHeight / 2 - lineHeight, linePaint!!
        )
    }

    @Synchronized
    private fun drawList(canvas: Canvas) {
        if (isClearing) return
        try {
            for (itemObject in itemList!!) {
                itemObject.drawSelf(canvas, measuredWidth)
            }
        } catch (e: Exception) {
            Log.e("WheelView", "drawList: $e")
        }
    }

    /**
     * 绘制遮盖板
     *
     * @param canvas
     */

    private fun drawMask(canvas: Canvas) {
        val colorArray = intArrayOf(0x0042C8FF, 0x3D42C8FF, 0x0042C8FF)
        val positionArray = floatArrayOf(0f, 0.5f, 1f)
        val lg3 = LinearGradient(
            0f, 0f,
            controlWidth, 0f, colorArray, positionArray, TileMode.MIRROR
        )
        val paint3 = Paint()
        paint3.shader = lg3
        canvas.drawRect(
            0f, controlHeight / 2 - unitHeight / 2 + lineHeight, controlWidth,
            controlHeight / 2 + unitHeight / 2 - lineHeight, paint3
        )
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (!isEnable) return true
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                isScrolling = true
                downY = event.y.toInt()
                downTime = System.currentTimeMillis()
            }

            MotionEvent.ACTION_MOVE -> {
                actionMove(y - downY)
                onSelectListener()
            }

            MotionEvent.ACTION_UP -> {
                val move = Math.abs(y - downY)
                // 判断这段时间移动的距离
                if (System.currentTimeMillis() - downTime < goonTime && move > goonDistance) {
                    goonMove(y - downY)
                } else {
                    actionUp(y - downY)
                    noEmpty()
                    isScrolling = false
                }
            }

            else -> {}
        }
        return true
    }

    /**
     * 继续移动一定距离
     */
    @Synchronized
    private fun goonMove(move: Int) {
        Thread {
            var distance = 0
            while (distance < unitHeight * MOVE_NUMBER) {
                try {
                    Thread.sleep(5)
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
                actionThreadMove(if (move > 0) distance else distance * -1)
                distance += 10
            }
            actionUp(if (move > 0) distance - 10 else distance * -1 + 10)
            noEmpty()
        }.start()
    }

    /**
     * 不能为空,必须有选项
     */
    private fun noEmpty() {
        if (!noEmpty) return
        for (item in itemList!!) {
            if (item.isSelected) return
        }
        val move = itemList[0].moveToSelected().toInt()
        if (move < 0) {
            defaultMove(move)
        } else {
            defaultMove(
                itemList[itemList.size - 1]
                    .moveToSelected().toInt()
            )
        }
        for (item in itemList) {
            if (item.isSelected) {
                if (onSelectListener != null) onSelectListener!!.endSelect(item.id, item.itemText)
                break
            }
        }
    }

    /**
     * 移动的时候
     *
     * @param move
     */
    private fun actionMove(move: Int) {
        for (item in itemList!!) {
            item.move(move)
        }
        invalidate()
    }

    /**
     * 移动,线程中调用
     *
     * @param move
     */
    private fun actionThreadMove(move: Int) {
        for (item in itemList!!) {
            item.move(move)
        }
        val rMessage = Message()
        rMessage.what = REFRESH_VIEW
        mHandler.sendMessage(rMessage)
    }

    /**
     * 松开的时候
     *
     * @param move
     */
    private fun actionUp(move: Int) {
        var newMove = 0
        if (move > 0) {
            for (i in itemList!!.indices) {
                if (itemList[i].isSelected) {
                    newMove = itemList[i].moveToSelected().toInt()
                    if (onSelectListener != null) onSelectListener!!.endSelect(
                        itemList[i].id,
                        itemList[i].itemText
                    )
                    break
                }
            }
        } else {
            for (i in itemList!!.indices.reversed()) {
                if (itemList[i].isSelected) {
                    newMove = itemList[i].moveToSelected().toInt()
                    if (onSelectListener != null) onSelectListener!!.endSelect(
                        itemList[i].id,
                        itemList[i].itemText
                    )
                    break
                }
            }
        }
        for (item in itemList) {
            item.newY(move + 0)
        }
        slowMove(newMove)
        val rMessage = Message()
        rMessage.what = REFRESH_VIEW
        mHandler.sendMessage(rMessage)
    }

    /**
     * 缓慢移动
     *
     * @param move
     */
    @Synchronized
    private fun slowMove(move: Int) {
        Thread { // 判断正负
            var m = if (move > 0) move else move * -1
            val i = if (move > 0) 1 else -1
            // 移动速度
            val speed = 1
            while (true) {
                m -= speed
                if (m <= 0) {
                    for (item in itemList!!) {
                        item.newY(m * i)
                    }
                    val rMessage = Message()
                    rMessage.what = REFRESH_VIEW
                    mHandler.sendMessage(rMessage)
                    try {
                        Thread.sleep(2)
                    } catch (e: InterruptedException) {
                        e.printStackTrace()
                    }
                    break
                }
                for (item in itemList!!) {
                    item.newY(speed * i)
                }
                val rMessage = Message()
                rMessage.what = REFRESH_VIEW
                mHandler.sendMessage(rMessage)
                try {
                    Thread.sleep(2)
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
            }
            if (itemList != null) {
                for (item in itemList) {
                    if (item.isSelected) {
                        if (onSelectListener != null) onSelectListener!!.endSelect(
                            item.id,
                            item.itemText
                        )
                        break
                    }
                }
            }
        }.start()
    }

    /**
     * 移动到默认位置
     *
     * @param move
     */
    private fun defaultMove(move: Int) {
        for (item in itemList!!) {
            item.newY(move)
        }
        val rMessage = Message()
        rMessage.what = REFRESH_VIEW
        mHandler.sendMessage(rMessage)
    }

    /**
     * 滑动监听
     */
    private fun onSelectListener() {
        if (onSelectListener == null) return
        for (item in itemList!!) {
            if (item.isSelected) {
                onSelectListener!!.selecting(item.id, item.itemText)
            }
        }
    }

    /**
     * 设置数据 (第一次)
     *
     * @param data
     */
    fun setData(data: MutableList<String>) {
        dataList = data
        initData()
    }

    /**
     * 重置数据
     *
     * @param data
     */
    fun refreshData(data: MutableList<String>) {
        setData(data)
        invalidate()
    }

    /**
     * 获取返回项 id
     *
     * @return
     */
    val selected: Int
        get() {
            for (item in itemList!!) {
                if (item.isSelected) return item.id
            }
            return -1
        }

    /**
     * 获取返回的内容
     *
     * @return
     */
    val selectedText: String
        get() {
            for (item in itemList!!) {
                if (item.isSelected) return item.itemText
            }
            return ""
        }

    /**
     * 设置默认选项
     *
     * @param index
     */
    fun setDefault(index: Int) {
        if (index > itemList!!.size - 1) return
        val move = itemList[index].moveToSelected()
        defaultMove(move.toInt())
    }

    /**
     * 获取列表大小
     *
     * @return
     */
    val listSize: Int
        get() = itemList?.size ?: 0

    /**
     * 获取某项的内容
     *
     * @param index
     * @return
     */
    fun getItemText(index: Int): String {
        return itemList?.get(index)?.itemText ?: ""
    }

    /**
     * 监听
     *
     * @param onSelectListener
     */
    fun setOnSelectListener(onSelectListener: OnSelectListener?) {
        this.onSelectListener = onSelectListener
    }


    var mHandler: Handler =
        object : Handler() {
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                when (msg.what) {
                    REFRESH_VIEW -> invalidate()
                    else -> {}
                }
            }
        }

    /**
     * 单条内容
     */
    private inner class ItemObject {
        /**
         * id
         */
        var id = 0

        /**
         * 内容
         */
        var itemText = ""

        /**
         * x坐标
         */
        var x = 0

        /**
         * y坐标
         */
        var y = 0

        /**
         * 移动距离
         */
        var move = 0

        /**
         * 字体画笔
         */
        private var textPaint: TextPaint? = null

        /**
         * 字体范围矩形
         */
        private var textRect: Rect? = null

        /**
         * 绘制自身
         *
         * @param canvas         画板
         * @param containerWidth 容器宽度
         */
        fun drawSelf(canvas: Canvas, containerWidth: Int) {
            if (textPaint == null) {
                textPaint = TextPaint()
                textPaint!!.isAntiAlias = true
            }
            if (textRect == null) textRect = Rect()

            // 判断是否被选择
            if (isSelected) {
                textPaint!!.color = selectedColor
                // 获取距离标准位置的距离
                var moveToSelect = moveToSelected()
                moveToSelect = if (moveToSelect > 0) moveToSelect else moveToSelect * -1
                // 计算当前字体大小
                val textSize =
                    normalFont + (selectedFont - normalFont) * (1.0f - moveToSelect / unitHeight.toFloat())
                textPaint!!.textSize = textSize
            } else {
                textPaint!!.color = normalColor
                textPaint!!.textSize = normalFont
            }


            // 判断是否可视
            if (!isInView) return

            //判断是一行还是两行,两行数据用,分割
            if (itemText.indexOf(",") != -1) {
                var (text1, text2) = itemText.split(",")


                // 返回包围整个字符串的最小的一个Rect区域
                text1 = TextUtils.ellipsize(
                    text1,
                    textPaint,
                    containerWidth.toFloat(),
                    TextUtils.TruncateAt.END
                ) as String
                textPaint!!.getTextBounds(text1, 0, text1.length, textRect)
                //双排文字一
                canvas.drawText(
                    text1,
                    x + controlWidth / 2 - textRect!!.width() / 2,
                    (y + move + unitHeight / 5 * 2 + textRect!!.height() / 5 * 2).toFloat(),
                    textPaint!!
                )
                // 返回包围整个字符串的最小的一个Rect区域
                text2 = TextUtils.ellipsize(
                    text2,
                    textPaint,
                    containerWidth.toFloat(),
                    TextUtils.TruncateAt.END
                ) as String
                textPaint!!.getTextBounds(text2, 0, text2.length, textRect)
                //双排文字2
                canvas.drawText(
                    text2,
                    x + controlWidth / 2 - textRect!!.width() / 2,
                    (y + move + unitHeight / 5 * 3 + textRect!!.height() / 5 * 3).toFloat(),
                    textPaint!!
                )
            } else {
                // 返回包围整个字符串的最小的一个Rect区域
                itemText = TextUtils.ellipsize(
                    itemText,
                    textPaint,
                    containerWidth.toFloat(),
                    TextUtils.TruncateAt.END
                ) as String
                textPaint!!.getTextBounds(itemText, 0, itemText.length, textRect)
                // 绘制内容
                canvas.drawText(
                    itemText, x + controlWidth / 2 - textRect!!.width() / 2,
                    (y + move + unitHeight / 2 + textRect!!.height() / 2).toFloat(), textPaint!!
                )
            }


        }

        /**
         * 是否在可视界面内
         *
         * @return
         */
        val isInView: Boolean
            get() = if (y + move > controlHeight || y + move + unitHeight / 2 + textRect!!.height() / 2 < 0) false else true

        /**
         * 移动距离
         *
         * @param _move
         */
        fun move(_move: Int) {
            move = _move
        }

        /**
         * 设置新的坐标
         *
         * @param _move
         */
        fun newY(_move: Int) {
            move = 0
            y = y + _move
        }

        /**
         * 判断是否在选择区域内
         *
         * @return
         */
        val isSelected: Boolean
            get() {
                if (y + move + unitHeight >= controlHeight / 2 - unitHeight / 2 + lineHeight
                    && y + move + unitHeight <= controlHeight / 2 + unitHeight / 2 - lineHeight
                ) {
                    return true
                }
                return (y + move <= controlHeight / 2 - unitHeight / 2 + lineHeight
                        && y + move + unitHeight >= controlHeight / 2 + unitHeight / 2 - lineHeight)
            }

        /**
         * 获取移动到标准位置需要的距离
         */
        fun moveToSelected(): Float {
            return controlHeight / 2 - unitHeight / 2 - (y + move)
        }
    }

    /**
     * 选择监听
     *
     * @author JiangPing
     */
    interface OnSelectListener {
        /**
         * 结束选择
         *
         * @param id
         * @param text
         */
        fun endSelect(id: Int, text: String?)

        /**
         * 选中的内容
         *
         * @param id
         * @param text
         */
        fun selecting(id: Int, text: String?)
    }

    companion object {
        /**
         * 刷新界面
         */
        private const val REFRESH_VIEW = 0x001

        /**
         * 移动距离
         */
        private const val MOVE_NUMBER = 5
    }
}

总结

主要还是考查自定义view相关能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/121386.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flink1.18.0 sql-client报错

报错 Flink SQL> select * from t1; [ERROR] Could not execute SQL statement. Reason: org.apache.flink.table.api.ValidationException: Could not find any factory for identifier kafka that implements org.apache.flink.table.factories.DynamicTableFactory in t…

STM32_project:led_beep

代码&#xff1a; 主要部分&#xff1a; #include "stm32f10x.h" // Device header #include "delay.h"// 给蜂鸣器IO口输出低电平&#xff0c;响&#xff0c;高&#xff0c;不向。 //int main (void) //{ // // 开启时钟 // RC…

如何在Linux上搭建本地Docker Registry并实现远程连接

Linux 本地 Docker Registry本地镜像仓库远程连接 文章目录 Linux 本地 Docker Registry本地镜像仓库远程连接1. 部署Docker Registry2. 本地测试推送镜像3. Linux 安装cpolar4. 配置Docker Registry公网访问地址5. 公网远程推送Docker Registry6. 固定Docker Registry公网地址…

【python算法】迪杰斯特拉算法 python实现

迪杰斯特拉算法 文章目录 迪杰斯特拉算法简介核心思想贪心算法的优缺点运行过程代码伪代码Python代码 简介 迪杰斯特拉算法的是用于图搜索的一种算法&#xff0c;其作用是图中搜索出单源最短路径。单源最短路径问题是一个给定起始点和目标点&#xff0c;在图中搜索出由起始点到…

4.Swin Transformer目标检测——训练数据集

1.centos7 安装显卡驱动、cuda、cudnn-CSDN博客 2.安装conda python库-CSDN博客 3.Cenots Swin-Transformer-Object-Detection环境配置-CSDN博客 步骤1&#xff1a;准备待训练的coco数据集 下载地址&#xff1a;https://download.csdn.net/download/malingyu/88519420 htt…

尼得科电机的强大性能,将列车门和屏蔽门的开合变得从容而安全

城市脉动&#xff0c;人流涌动。 无论城市轨道交通还是远途铁路运输&#xff0c; 尼得科电机的强大性能&#xff0c;将列车门和屏蔽门的开合变得从容而安全。 尼得科的电机方案&#xff0c;有助于列车门稳准开闭&#xff0c;保障乘客安全无忧。高效驱动&#xff0c;让乘客的行程…

Win10笔记本开热点后电脑断网的解决方法

在Win10笔记本电脑中用户可以随时打开热点&#xff0c;但是发现热点开启后电脑就会断网&#xff0c;网络不稳定就会影响到用户的正常使用。下面小编给大家介绍两种简单的解决方法&#xff0c;解决后用户在Win10笔记本电脑开热点就不会有断网的问题出现了。 具体解决方法如下&am…

基于FPGA的PCIe-Aurora 8/10音频数据协议转换系统设计阅读笔记

文章可知网下载阅读&#xff0c;该论文设计了一种 PC 到光纤模块&#xff08;基于Aurora的光纤传输&#xff09;的数据通路&#xff0c;成功完成了Aurora 以及 DDR 等模块的功能验证。 学习内容&#xff1a; 本次主要学习了Pcie高速串行总线协议、Aurora高速串行总线协议、DDR相…

在直播系统中使用RTSP协议传递视频

目录 概述 1、环境准备 2、拉流URL地址 3、导播软件取流 &#xff08;1&#xff09;OBS中拉取RTSP流 &#xff08;2&#xff09;芯象中拉取RTSP流 &#xff08;3&#xff09;vMix中拉取RTSP流 写在最后 概述 提到RTSP协议&#xff0c;很容易想到RTMP协议&#xff0c;它…

Grafana安装配置

配置文件路径 /etc/grafana/grafana.ini 一、Grafana 安装 https://grafana.com/grafana/download?editionoss&pgget&plcmtselfmanaged-box1-cta1 以centos为例 #安装 sudo yum install -y https://dl.grafana.com/oss/release/grafana-10.2.0-1.x86_64.rpm#修改端…

正点原子嵌入式linux驱动开发——Linux WIFI驱动

WIFI的使用已经很常见了&#xff0c;手机、平板、汽车等等&#xff0c;虽然可以使用有线网络&#xff0c;但是有时候很多设备存在布线困难的情况&#xff0c;此时WIFI就是一个不错的选择。正点原子STM32MP1开发板支持USB和SDIO这两种接口的WIFI&#xff0c;本章就来学习一下如何…

Maven 从入门到精通

目录 一. 前言 二. Maven 下载与安装 2.1. 下载 2.2. 安装 三. Maven 核心概念 3.1. POM 3.2. 约定的目录结构 3.3. 坐标 3.4. 依赖管理 3.4.1. 直接依赖和间接依赖 3.4.2. 依赖的排除 3.4.3. 统一的版本管理 3.4.4. 依赖范围 3.5. 仓库 3.6. 生命周期/插件/目标…

JavaSpringBootMysql大学生兼职平台94598-计算机毕业设计项目选题推荐(附源码)

摘 要 当今人类社会已经进入信息全球化和全球信息化、网络化的高速发展阶段。丰富的网络信息已经成为人们工作、生活、学习中不可缺少的一部分。人们正在逐步适应和习惯于网上贸易、网上购物、网上支付、网上服务和网上娱乐等活动&#xff0c;人类的许多社会活动正在向网络化发…

iview实现table里面每行数据的跳转

我的需求是跳转到第三方网站&#xff0c;看官方是写了如何跳转站内路由&#xff0c;不符合我的要求&#xff0c;在csdn发现了一篇文章&#xff0c;我贴一下代码 <template><Table border :columns"ReportColumns" :data"ReportData"><templ…

vue3 - swiper插件 实现PC端的 视频滑动功能(仿抖音短视频)

swiper官网 ​​​​​​swiper属性/组件查询 vue中使用swiper 步骤&#xff1a; ① npm install swiper 安装 ② 基础模板&#xff1a; <div><swiper class"swiper-box" :direction"vertical":grabCursor"true" :mousewheel"tr…

【Mybatis小白从0到90%精讲】12:Mybatis删除 delete, 推荐使用主键删除!

文章目录 前言XML映射文件方式推荐使用主键删除注解方式工具类前言 在实际开发中,我们经常需要删除数据库中的数据,MyBatis可以使用XML映射文件或注解来编写删除(delete)语句,下面是两种方法的示例。 XML映射文件方式 Mapper: int delete(int id);Mapper.xml:

使用easyui前端框架快速构建一个crud应用

本篇文章将会详细介绍jquery easyui前端框架的使用&#xff0c;通过创建一个crud应用来带大家快速掌握easyui的使用。 easyui是博主最喜欢的前端框架&#xff0c;没有之一&#xff0c;因为它提供了多种主题&#xff0c;而且圆润可爱的组件吸引了我。 快速开始 easyui的官网地址…

css控制卡片内部的左右布局

先放效果图 纯css样式 可以根据需求进行更改 <template> <!-- 卡片盒子 --><div class"card_box "><el-card class"box-card w400" v-for"(item,index) in cardList" :key"index"><div slot"heade…

山西电力市场日前价格预测【2023-11-09】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-11-09&#xff09;山西电力市场全天平均日前电价为369.84元/MWh。其中&#xff0c;最高日前电价为784.47元/MWh&#xff0c;预计出现在17: 45。最低日前电价为158.90元/MWh&#xff0c;预计…

【Python爬虫库】pytube使用方法

一、pytube库简介 pytube库是一个python第三方库&#xff0c;用于youtube视频的抓取和其他相关操作。官方文档&#xff1a;pytube 二、基本操作 1、显示视频标题 from pytube import YouTube yt YouTube(https://youtube.com/watch?vIAJsZWhj6GI) print(yt.title)说明&am…