五分钟实现Android超漂亮的刻度轮播控件

作者 : 开心源码 本文共18090个字,预计阅读时间需要46分钟 发布时间: 2022-05-12 共120人阅读

??欢迎大家关注一下我开源的一个音视频库,HardwareVideoCodec是一个高效的Android音视频编码库,支持软编和硬编。用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳固版本,欢迎查阅学习和用,如有BUG或者建议,欢迎Issue。

??最近一直在做音视频的工作,已经有大半年没有写应使用层的东西了,生怕越来越生疏。正好前段时间接了个外包项目,才得以回顾一下。项目中有一个控件挺简洁漂亮的,而且使用到的技术也比较基础,比较适合新手学习,所以单独开源出来,希望能对初学者有所帮助。

截图截屏

一、自己设置View的常使用方法

??说到自己设置View,总绕不开下面几个方法

1. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)

??初始化View时,使用于测量大小,并对View的大小进行控制,比方可以控制View的宽高比例。

2. override fun onDraw(canvas: Canvas)

??View的绘制回调,所有的画笔、画布操作都在这里。切勿在此方法进行耗时操作,能在外部计算的都在外部计算,并且尽量不要在这里初始化变量。由于正常情况下这个方法会以60fps的速度进行回调,假如有耗时操作,将会卡慢,假如初始化大量对象,则会消耗大量内存。总之,跟画布无关的操作都不要写在这里。

3. invalidate()

??使用于通知View进行重绘,也就是重新调使用onDraw,当我们界面属性发生变化时,即可以调使用该方法来进行重绘,而不是调使用onDraw,这个方法非常常使用。

4. override fun onTouchEvent(event: MotionEvent): Boolean

??相信大家都知道,这个是触摸事件回调。在这里可以解决少量手势操作。

二、自己设置一个刻度控件RulerView

??因为代码比较多,而且源码里面的注释也比较详细,所以这里只挑重点的几个方法讲解一下。假如有问题,或者者错误,欢迎在评论区留言。
??观察本文开始的视频,我们可以发现,该控件尽管看起来挺简洁,但是需要控制的部分却不少,光刻度就有三种类型,还有少量文字。

  1. 普通刻度,宽度比较短,颜色比较浅,不带文字。
  2. 整10刻度,宽度比较长,颜色相较普通刻度深一点,并且带有文字。
  3. 游标刻度,宽度在三类刻度里面是最长的,颜色高亮,并且也带有文字。
  4. 标签文字,使用于形容该刻度的使用途。

??以上都是需要我们使用画笔来绘制的,所以我们定义了以下几个画笔,为了避免在onDraw中频繁更改画笔属性,这里又对文字和刻度定义了单独的画笔,目的是避免任何画笔属性的改变和在onDraw中改变属性导致绘制过于耗时,更重要的是来回更改画笔的属性过于复杂,不便于操作和问题排查。

  1. scalePaint: Paint //刻度画笔
  2. scalePointerPaint: Paint //整10刻度文字画笔
  3. scalePointerTextPaint: Paint //整10刻度文字画笔
  4. cursorPaint: Paint //游标画笔
  5. cursorTextPaint: Paint //游标文字画笔
  6. cursorLabelPaint: Paint //标签文字画笔

1、从xml设置的属性初始化参数

??除了基础的画笔对象,还需要少量画笔必要的属性,比方我们绘制一个刻度,需要知道刻度位置、大小和间距。所以围绕这些,又定义了一系列属性。这些属性可以由xml定义时提供,由此引出View的另一个重要使用法。
??这个使用法比较固定,都是这个套路。其中需要注意的是,相似于R.styleable.app_scaleWidth这种id是在values/attrs.xml中定义的,app代表命名空间,可以自己设置,scaleWidth就是属性id,跟layout_width这些是一样的。我们在一个命名空间中定义了一个属性id后,即可以像用layout_widthlayout_height那样从xml中向View传递属性了。此时在View的构造方法中可以直接获取这些属性值,代码如下。

/** * 从xml设置的属性初始化参数 */private fun resolveAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {    scaleStrokeWidth = dpToPx(context, 1f) / 2f    scaleWidth = 50    scalePointerWidth = (scaleWidth * 1.5).toInt()    cursorWidth = (scaleWidth * 3.333).toInt()    scaleHeight = 5    cursorColor = context.resources.getColor(R.color.red)    scaleColor = context.resources.getColor(R.color.grey_888)    scalePointerColor = context.resources.getColor(R.color.grey_800)    val a = context.theme.obtainStyledAttributes(attrs, R.styleable.app, defStyleAttr, defStyleRes)    for (i in 0 until a.indexCount) {        val attr = a.getIndex(i)        if (attr == R.styleable.app_scaleWidth) {            scaleWidth = a.getDimensionPixelOffset(attr, 50)            scalePointerWidth = (scaleWidth * 1.5).toInt()            cursorWidth = (scaleWidth * 3.333).toInt()        } else if (attr == R.styleable.app_scaleHeight) {            scaleHeight = a.getDimensionPixelOffset(attr, 5)        } else if (attr == R.styleable.app_cursorColor) {            cursorColor = a.getColor(attr, context.resources.getColor(R.color.red))        } else if (attr == R.styleable.app_scaleColor) {            scaleColor = a.getColor(attr, context.resources.getColor(R.color.grey_888))        } else if (attr == R.styleable.app_scalePointerColor) {            scalePointerColor = a.getColor(attr, context.resources.getColor(R.color.grey_800))        }    }    cursorTextOffsetLeft = dpToPx(context, 32f)    a.recycle()}

2、绘制View

??本文并没有用View提供的scrollToscrollBy来控制滚动,而是重新定义一个x,y属性来记录滚动位置,通过这个属性绘制相应的位置,来实现滚动效果。这样操作可以通过指定绘制区域(屏幕外的内容不绘制,感兴趣的同学可以去尝试实现)来处理性能问题。
??drawScale通过遍历items来绘制每一个元素,包括刻度和对应的文字,都是比较基本的操作。需要注意的是canvas.drawText默认情况下的x,y是指文字的左下角位置。

private fun drawScale(canvas: Canvas) {    for (i in 0 until items.size) {//根据给定的item信息绘制刻度        val top = offsetHeight + mCurrentOrigin.y.toInt() + i * scaleHeight + scaleHeight / 2        if (0 == i % 10) {//绘制整10刻度            canvas.drawRect(RectF(pointerScaleLeft.toFloat(), top - scaleStrokeWidth,                    pointerScaleLeft.toFloat() + scalePointerWidth,                    top + scaleStrokeWidth),                    scalePointerPaint)            if (Math.abs(getSelectedItem() - i) > 1) {//整10刻度有文字,所以需要计算文字位置,并绘制文字                val text = items[i].toString()                val size = measureTextSize(scalePointerTextPaint, text)                canvas.drawText(text, pointerScaleLeft - size[0] * 1.3f, top + size[1] / 2, scalePointerTextPaint)            }        } else {//绘制普通刻度            canvas.drawRect(RectF(scaleLeft.toFloat(), top - scaleStrokeWidth,                    scaleLeft.toFloat() + scaleWidth,                    top + scaleStrokeWidth),                    scalePaint)        }    }}/** * 绘制游标,这里也需要计算文字位置,包括item文字和标签文字 */private fun drawCursor(canvas: Canvas) {    val left = scaleLeft + scaleWidth - cursorWidth    val top = measuredHeight / 2f    canvas.drawRect(RectF(left.toFloat(), top.toInt() - scaleStrokeWidth,            left.toFloat() + cursorWidth, top.toInt() + scaleStrokeWidth),            cursorPaint)    val text = items[getSelectedItem()].toString()    val textSize = measureTextSize(cursorTextPaint, text)    val labelSize = measureTextSize(cursorLabelPaint, label)    val labelLeft = left - cursorTextOffsetLeft - labelSize[0]    val textOffset = (textSize[0] - labelSize[0]) / 2f    canvas.drawText(text, left - cursorTextOffsetLeft - textSize[0] + textOffset, top + textSize[1] / 2, cursorTextPaint)    canvas.drawText(label, labelLeft, top + textSize[1] + labelSize[1], cursorLabelPaint)}

3、支持滚动

??Android的手势滚动操作比较简单,不需要自己去实现各种逻辑控制,而是通过系统提供的Scroller来计算滚动位置。
??首先我们需要一个GestureDetectorCompatOverScroller,前者使用于手势监听,后者通过MotionEvent来计算滚动位置。

1.mGestureDetector: GestureDetectorCompat

  1. scroller: OverScroller
private fun init() {    mGestureDetector = GestureDetectorCompat(context, onGestureListener)    scroller = OverScroller(context)}

??构造一个GestureDetectorCompat对象,需要先提供一个OnGestureListener,使用来监听onScrollonFling事件。其实就是MotionEvent经过GestureDetectorCompat解决之后,就变成了可以直接用的滚动和惯性滚动事件,而后通过这两个回调通知我们。
??在onScroll中,我们通过横向和纵向滚动距离来计算滚动方向,假如横向滚动距离大于纵向滚动距离,我们则可以认为是横向滚动,反之则是纵向滚动。本文只要要纵向滚动。
??拿到滚动方向之后,我们即可以对滚动位置x,y进行累加,记录每一次滑动之后的新的位置。最后通过postInvalidateOnAnimation或者invalidate来通知重新绘制,onDraw根据新的x,y绘制对应位置的画面,来实现滑动。
??尽管通过onScroll已经实现了View的滑动,但只是实现跟随手指运动,还没有实现“抛”的动作。在现实世界中,运动是有惯性的,假如只实现onScroll,一切都显得很生硬。那么如何实现惯性运动呢,我们自己计算?想想都可怕,这么多运动函数,相信不是一般人能应付的来的。幸运的是,这个计算我们可以交给GestureDetectorCompat的onFling
??onFling有四个参数,前两个是MotionEvent,分别代表前后两个触摸事件。velocityX: Float代表X轴滚动速率,velocityY: Float代表Y轴滚动速率,我们不需要关心这两个值如何,直接交给scroller解决就可。
??这里也许有人要问了,我们的手指离开屏幕之后便不再产生事件,View是如何实现持续滑动的呢。再回头看一下onFling回调也的确如此,onFling只会根据手指离开屏幕前两个MotionEvent来计算速率,之后就再也没有回调,所以scroller.fling也仅仅是调使用了一次,并不能持续滚动。那我们如何实现持续的惯性滚动呢?
??要实现持续的惯性滚动,就得依赖于override fun computeScroll(),该方法由draw过程中调使用,我们可以通过invalidate->onDraw->computeScroll->invalidate这样一个循环来控制惯性滚动,直至惯性滚动中止,具体实现可以参考文章最后的源码。

/** * 手势监听 */private val onGestureListener = object : GestureDetector.SimpleOnGestureListener() {    /**     * 手指按下回调,这里将状态标记为非滚动状态     */    override fun onDown(e: MotionEvent): Boolean {        parent.requestDisallowInterceptTouchEvent(true)        mCurrentScrollDirection = Direction.NONE        return true    }    /**     * 手指拖动回调     */    override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {        //假如当前正在滚动,则中止滚动        scroller.forceFinished(true)//            Log.e(RulerView::class.java, "onScroll: ${mCurrentOrigin.y}, $distanceY")        if (Direction.NONE == mCurrentScrollDirection) {//判断滚动方向,这里只有垂直一个方向            mCurrentScrollDirection = if (Math.abs(distanceX) < Math.abs(distanceY)) {                Direction.VERTICAL            } else {                Direction.NONE            }        }        // Calculate the new origin after scroll.        when (mCurrentScrollDirection) {            Direction.VERTICAL -> {//计算手指拖动距离,并记录新的坐标重绘界面                mCurrentOrigin.y -= distanceY                checkOriginY()                ViewCompat.postInvalidateOnAnimation(this@RulerView)            }        }        return true    }    /**     * 惯性滚动回调     */    override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {        scroller.forceFinished(true)        mCurrentFlingDirection = mCurrentScrollDirection        when (mCurrentFlingDirection) {            Direction.VERTICAL -> scroller.fling(mCurrentOrigin.x.toInt(), mCurrentOrigin.y.toInt(),                    0, velocityY.toInt(), Integer.MIN_VALUE,                    Integer.MAX_VALUE, Integer.MIN_VALUE, 0)        }        ViewCompat.postInvalidateOnAnimation(this@RulerView)        return true    }}

??至此自己设置View的绘制事件两个重要部分都讲完了。喜欢的话记得点赞、评论和关注,您的关注是我的鼓励。文章最后贴出相关源码,欢迎查阅学习。假如有问题,或者者错误,欢迎在评论区留言。

完整代码

??RulerView.kt

class RulerView : View {    private enum class Direction {        NONE, VERTICAL    }    private var label: String = "LABEL"    private var items: List<*> = ItemCreator.range(0, 60)    //游标颜色    private var cursorColor = 0    //刻度颜色    private var scaleColor = 0    //整10刻度颜色    private var scalePointerColor = 0    //可滚动高度    private var scrollHeight = 0f    //刻度宽度    private var scaleWidth = 0    //整10刻度宽度    private var scalePointerWidth = 0    //游标宽度    private var cursorWidth = 0    //刻度高度+刻度间距    private var scaleHeight = 0    //刻度高度    private var scaleStrokeWidth = 0f    //刻度画笔    private lateinit var scalePaint: Paint    //整10刻度画笔    private lateinit var scalePointerPaint: Paint    //整10刻度文字画笔    private lateinit var scalePointerTextPaint: Paint    //游标画笔    private lateinit var cursorPaint: Paint    //游标文字画笔    private lateinit var cursorTextPaint: Paint    //标签文字画笔    private lateinit var cursorLabelPaint: Paint    //刻度间距    private var offsetHeight = 0    //刻度与文字的间距    private var cursorTextOffsetLeft = 0    //刻度距离View左边的距离    private var scaleLeft = 0    //整10刻度距离View左边的距离    private var pointerScaleLeft = 0    //滚动控制器    private lateinit var scroller: OverScroller    private var maxFlingVelocity = 0    private var minFlingVelocity = 0    private var touchSlop = 0    //当前滚动方向    private var mCurrentScrollDirection = Direction.NONE    //当前惯性滚动方向    private var mCurrentFlingDirection = Direction.NONE    //当前滚动x,y    private val mCurrentOrigin = PointF(0f, 0f)    //手势支持    private lateinit var mGestureDetector: GestureDetectorCompat    constructor(context: Context) : super(context) {        resolveAttribute(context, null, 0, 0)        init()    }    constructor(context: Context, attrs: AttributeSet?)            : super(context, attrs) {        resolveAttribute(context, attrs, 0, 0)        init()    }    constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int)            : super(context, attrs, defStyleAttr) {        resolveAttribute(context, attrs, defStyleAttr, 0)        init()    }    /**     * 从xml属性初始化参数     */    private fun resolveAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {        scaleStrokeWidth = dpToPx(context, 1f) / 2f        scaleWidth = 50        scalePointerWidth = (scaleWidth * 1.5).toInt()        cursorWidth = (scaleWidth * 3.333).toInt()        scaleHeight = 5        cursorColor = context.resources.getColor(R.color.red)        scaleColor = context.resources.getColor(R.color.grey_888)        scalePointerColor = context.resources.getColor(R.color.grey_800)        val a = context.theme.obtainStyledAttributes(attrs, R.styleable.app, defStyleAttr, defStyleRes)        for (i in 0 until a.indexCount) {            val attr = a.getIndex(i)            if (attr == R.styleable.app_scaleWidth) {                scaleWidth = a.getDimensionPixelOffset(attr, 50)                scalePointerWidth = (scaleWidth * 1.5).toInt()                cursorWidth = (scaleWidth * 3.333).toInt()            } else if (attr == R.styleable.app_scaleHeight) {                scaleHeight = a.getDimensionPixelOffset(attr, 5)            } else if (attr == R.styleable.app_cursorColor) {                cursorColor = a.getColor(attr, context.resources.getColor(R.color.red))            } else if (attr == R.styleable.app_scaleColor) {                scaleColor = a.getColor(attr, context.resources.getColor(R.color.grey_888))            } else if (attr == R.styleable.app_scalePointerColor) {                scalePointerColor = a.getColor(attr, context.resources.getColor(R.color.grey_800))            }        }        cursorTextOffsetLeft = dpToPx(context, 32f)        a.recycle()    }    /**     * 初始化画笔、滚动控制器和手势对象     */    private fun init() {        scroller = OverScroller(context)        mGestureDetector = GestureDetectorCompat(context, onGestureListener)        maxFlingVelocity = ViewConfiguration.get(context).scaledMaximumFlingVelocity        minFlingVelocity = ViewConfiguration.get(context).scaledMinimumFlingVelocity        touchSlop = ViewConfiguration.get(context).scaledTouchSlop        scalePaint = Paint(Paint.ANTI_ALIAS_FLAG)        scalePaint.color = scaleColor        scalePaint.style = Paint.Style.FILL        scalePointerPaint = Paint(Paint.ANTI_ALIAS_FLAG)        scalePointerPaint.color = scalePointerColor        scalePointerPaint.style = Paint.Style.FILL        scalePointerTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)        scalePointerTextPaint.color = scaleColor        scalePointerTextPaint.style = Paint.Style.FILL        scalePointerTextPaint.textSize = spToPx(context, 14f).toFloat()        cursorPaint = Paint(Paint.ANTI_ALIAS_FLAG)        cursorPaint.color = cursorColor        cursorPaint.style = Paint.Style.FILL        cursorTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)        cursorTextPaint.color = context.resources.getColor(R.color.black_232)        cursorTextPaint.style = Paint.Style.FILL        cursorTextPaint.textSize = spToPx(context, 32f).toFloat()        cursorLabelPaint = Paint(Paint.ANTI_ALIAS_FLAG)        cursorLabelPaint.color = scalePointerColor        cursorLabelPaint.style = Paint.Style.FILL        cursorLabelPaint.textSize = spToPx(context, 16f).toFloat()    }    /**     * 设置item数据     */    fun setItems(items: List<*>) {        this.items = items        this.scrollHeight = (height + (this.items.size - 1) * scaleHeight).toFloat()        post {            mCurrentOrigin.x = 0f            mCurrentOrigin.y = 0f            invalidate()        }    }    /**     * 获取item数据     */    fun getItems(): List<*> {        return items    }    /**     * 设置标签文字     */    fun setLabel(label: String) {        this.label = label        //重新初始化刻度左距离        initScaleLeft()        //通知重新绘制        invalidate()    }    /**     * 触控事件交给mGestureDetector     */    override fun onTouchEvent(event: MotionEvent): Boolean {        val result = mGestureDetector.onTouchEvent(event)        //假如手指离开屏幕,并且没有惯性滑动        if (event.action == MotionEvent.ACTION_UP && mCurrentFlingDirection == Direction.NONE) {            if (mCurrentScrollDirection == Direction.VERTICAL) {                //检查能否需要对齐刻度                snapScroll()            }            mCurrentScrollDirection = Direction.NONE        }        return result    }    /**     * 计算View如何滑动     */    override fun computeScroll() {        super.computeScroll()        if (scroller.isFinished) {//滚动以及完成            if (mCurrentFlingDirection !== Direction.NONE) {                // Snap to day after fling is finished.                mCurrentFlingDirection = Direction.NONE                snapScroll()//检查能否需要对齐刻度,假如需要,则自动滚动,让游标与刻度对齐            }        } else {            //假如当前不处于滚动状态,则再次检查能否需要对齐刻度            if (mCurrentFlingDirection != Direction.NONE && forceFinishScroll()) {                snapScroll()            } else if (scroller.computeScrollOffset()) {//检查能否滚动完成,并且计算新的滚动坐标                mCurrentOrigin.y = scroller.currY.toFloat()//记录当前y坐标                checkOriginY()//检查坐标能否越界                ViewCompat.postInvalidateOnAnimation(this)//通知重新绘制            } else {//不作滚动                val startY = if (mCurrentOrigin.y > 0)                    0f                else if (mCurrentOrigin.y < height - measuredHeight)                    measuredHeight - scrollHeight                else                    mCurrentOrigin.y                scroller.startScroll(0, startY.toInt(), 0, 0, 0)            }        }    }    /**     * 测量控件大小,并初始化少量必要的属性     */    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec)        val width = measuredWidth        val height = measuredHeight        offsetHeight = height / 2 - scaleHeight / 2        scrollHeight = height + (items.size - 1f) * scaleHeight        initScaleLeft()        pointerScaleLeft = scaleLeft + scaleWidth - scalePointerWidth    }    /**     * 初始化刻度左间距     */    private fun initScaleLeft() {        val labelSize = measureTextSize(cursorLabelPaint, label)        scaleLeft = (measuredWidth - scalePointerWidth + cursorTextOffsetLeft + labelSize[0].toInt()) / 2    }    override fun onDraw(canvas: Canvas) {        super.onDraw(canvas)        if (items.isEmpty()) return        //绘制刻度        drawScale(canvas)        //绘制游标        drawCursor(canvas)    }    private fun drawScale(canvas: Canvas) {        for (i in 0 until items.size) {//根据给定的item信息绘制刻度            val top = offsetHeight + mCurrentOrigin.y.toInt() + i * scaleHeight + scaleHeight / 2            if (0 == i % 10) {//绘制整10刻度                canvas.drawRect(RectF(pointerScaleLeft.toFloat(), top - scaleStrokeWidth,                        pointerScaleLeft.toFloat() + scalePointerWidth,                        top + scaleStrokeWidth),                        scalePointerPaint)                if (Math.abs(getSelectedItem() - i) > 1) {//整10刻度有文字,所以需要计算文字位置,并绘制文字                    val text = items[i].toString()                    val size = measureTextSize(scalePointerTextPaint, text)                    canvas.drawText(text, pointerScaleLeft - size[0] * 1.3f, top + size[1] / 2, scalePointerTextPaint)                }            } else {//绘制普通刻度                canvas.drawRect(RectF(scaleLeft.toFloat(), top - scaleStrokeWidth,                        scaleLeft.toFloat() + scaleWidth,                        top + scaleStrokeWidth),                        scalePaint)            }        }    }    /**     * 绘制游标,这里也需要计算文字位置,包括item文字和标签文字     */    private fun drawCursor(canvas: Canvas) {        val left = scaleLeft + scaleWidth - cursorWidth        val top = measuredHeight / 2f        canvas.drawRect(RectF(left.toFloat(), top.toInt() - scaleStrokeWidth,                left.toFloat() + cursorWidth, top.toInt() + scaleStrokeWidth),                cursorPaint)        val text = items[getSelectedItem()].toString()        val textSize = measureTextSize(cursorTextPaint, text)        val labelSize = measureTextSize(cursorLabelPaint, label)        val labelLeft = left - cursorTextOffsetLeft - labelSize[0]        val textOffset = (textSize[0] - labelSize[0]) / 2f        canvas.drawText(text, left - cursorTextOffsetLeft - textSize[0] + textOffset, top + textSize[1] / 2, cursorTextPaint)        canvas.drawText(label, labelLeft, top + textSize[1] + labelSize[1], cursorLabelPaint)    }        private fun forceFinishScroll(): Boolean {        return scroller.currVelocity <= minFlingVelocity    }    /**     * 与刻度对齐     */    private fun snapScroll() {        scroller.computeScrollOffset()        val nearestOrigin = -getSelectedItem() * scaleHeight        mCurrentOrigin.y = nearestOrigin.toFloat()        ViewCompat.postInvalidateOnAnimation(this@RulerView)    }    /**     * 检查y坐标越界     */    private fun checkOriginY() {        if (mCurrentOrigin.y > 0) mCurrentOrigin.y = 0f        if (mCurrentOrigin.y < measuredHeight - scrollHeight)            mCurrentOrigin.y = measuredHeight - scrollHeight    }    /**     * 获取选中的item     */    fun getSelectedItem(): Int {        var index = -Math.round(mCurrentOrigin.y / scaleHeight)        if (index >= items.size) index = items.size - 1        if (index < 0) index = 0        return index    }    /**     * 设置选中item     */    fun setSelectedItem(index: Int) {        post {            mCurrentOrigin.y = -(scaleHeight * index).toFloat()            checkOriginY()            ViewCompat.postInvalidateOnAnimation(this@RulerView)            snapScroll()        }    }    /**     * 手势监听     */    private val onGestureListener = object : GestureDetector.SimpleOnGestureListener() {        /**         * 手指按下回调,这里将状态标记为非滚动状态         */        override fun onDown(e: MotionEvent): Boolean {            parent.requestDisallowInterceptTouchEvent(true)            mCurrentScrollDirection = Direction.NONE            return true        }        /**         * 手指拖动回调         */        override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {            //假如当前正在滚动,则中止滚动            scroller.forceFinished(true)//            Log.e(RulerView::class.java, "onScroll: ${mCurrentOrigin.y}, $distanceY")            if (Direction.NONE == mCurrentScrollDirection) {//判断滚动方向,这里只有垂直一个方向                mCurrentScrollDirection = if (Math.abs(distanceX) < Math.abs(distanceY)) {                    Direction.VERTICAL                } else {                    Direction.NONE                }            }            // Calculate the new origin after scroll.            when (mCurrentScrollDirection) {                Direction.VERTICAL -> {//计算手指拖动距离,并记录新的坐标重绘界面                    mCurrentOrigin.y -= distanceY                    checkOriginY()                    ViewCompat.postInvalidateOnAnimation(this@RulerView)                }            }            return true        }        /**         * 惯性滚动回调         */        override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {            scroller.forceFinished(true)            mCurrentFlingDirection = mCurrentScrollDirection            when (mCurrentFlingDirection) {                Direction.VERTICAL -> scroller.fling(mCurrentOrigin.x.toInt(), mCurrentOrigin.y.toInt(),                        0, velocityY.toInt(), Integer.MIN_VALUE,                        Integer.MAX_VALUE, Integer.MIN_VALUE, 0)            }            ViewCompat.postInvalidateOnAnimation(this@RulerView)            return true        }    }    class ItemCreator {        companion object {            fun range(start: Int, end: Int): List<*> {                val result = ArrayList<Int>()                (start..end).forEach {                    result.add(it)                }                return result            }        }    }    companion object {        fun dpToPx(context: Context, dp: Float): Int {            return Math.round(context.resources.displayMetrics.density * dp)        }        fun spToPx(context: Context, sp: Float): Int {            return (TypedValue.applyDimension(2, sp, context.resources.displayMetrics) + 0.5f).toInt()        }        /**         * 测量文字宽高         */        fun measureTextSize(paint: Paint, text: String): FloatArray {            if (TextUtils.isEmpty(text)) return floatArrayOf(0f, 0f)            val width = paint.measureText(text, 0, text.length)            val bounds = Rect()            paint.getTextBounds(text, 0, text.length, bounds)            return floatArrayOf(width, bounds.height().toFloat())        }    }}

??attrs.xml

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="app">        <attr name="scaleWidth" format="dimension" />        <attr name="scaleHeight" format="dimension" />        <attr name="cursorColor" format="color" />        <attr name="scaleColor" format="color" />        <attr name="scalePointerColor" format="color" />    </declare-styleable></resources>

??sample

<com.lava.demo.widget.RulerView    android:id="@+id/timeRuler"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="#FAFAFA"    app:cursorColor="#F48B2E"    app:scaleColor="#C9C9C9"    app:scaleHeight="8dp"    app:scalePointerColor="#999999"    app:scaleWidth="12dp" />

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 五分钟实现Android超漂亮的刻度轮播控件

发表回复