记一次自定义View:滑动标尺


前言

滑动标尺, 适用于对 身高体重 的记录。本文的主心逻辑, 源于网上别人的文章资料, 还是某开源库代码, 博主已经不记得了. 由于样式与业务UI设计图 相差甚远. 经过博主魔改, 最终效果如下:


在这里插入图片描述


一、分析

  1. 三个画笔, 一个绘制刻度值, 一个绘制普通刻度, 一个亮色刻度
  2. 距离中心较远的刻度, 绘制普通刻度线
  3. 距离中心较近的刻度, 用亮色画笔, 并按距离适当增加它的长度
  4. 监听 onTouchEvent 事件, 根据滑动距离. 切换绘制情况
  5. 使用Scroller(滑动辅助类), VelocityTracker(惯性类) 做惯性滑动

涉及问题点:

  1. 博主将好多参数写死了, 也可以改用自定义属性控制
  2. 超出屏幕的 刻度线, 我们不绘制. 避免浪费效率
  3. 竖向身高标尺的代码, 本文并未贴出. 将横向代码研究透彻后, 可以较方便的改为竖向标尺, 想实践的小伙伴 自行研究
  4. 刻度线的样式,可以随便定义. 比如让带刻度值的线长一点. 修改 onDraw 中的 canvas.drawLine() 代码即可

二、上代码

1.自定义View代码

注释大多都在代码当中, Scroller 的知识也可以自行度娘;

class MyRulerView : View{
    constructor(context: Context): this(context, null)
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
            : super(context, attrs, defStyleAttr)

    //************ Paint *************
    private val mTextPaint: Paint   // 刻度数值
    private val mScalePaint: Paint   // 刻度线 - 暗色
    private val mCenterScalePaint: Paint    // 刻度线 - 亮色


    //************** 刻度参数 **************
    private var mCurrentValue = 50.0f   // 刻度尺 当前值.
    private var mPerValue = 1f          // 刻度精度, 例如: 身高为1cm  体重为0.1Kg
    private var mMaxValue = 200f        // 刻度最大值上限
    private var mMinValue = 100f        // 刻度最小值
    private var mTotalScale = 0         // 共有多少条 刻度
    private var mOffset = 0f            // 刻度尺当前值 位于尺子总刻度的位置
    private var mMaxOffset = 0          // 所有刻度 共有多长

    private var mScaleSpace = 25f       // 刻度2条线之间的距离
    private var mScaleWidth = 4f        // 刻度线的宽度(粗细)
    private var mScaleHeight = 40f      // 刻度线的长度(基础长度)

    private var mTextDistance = 4f      // 文字 与刻度 之间的距离;

    private var mCenterColor = Color.parseColor("#fa6521")  // 亮色刻度 色值
    private var mTextColor = Color.parseColor("#cdcdcd")    // 文字, 普通刻度 的颜色

    var mVlaueListener: ((Float) -> Unit)? = null    // 滑动后数值回调

    // 刻度值 文字参数
    private var mTextSize = 30f         // 尺子刻度下方数字 textsize
    private val mTextHeight: Float      // 刻度数值文字 的高度
    private var mTextLoc = 0f           // 文字基线的位置;


    //************** 手势滑动相关 **************
    /**
     * Scroller是一个专门用于处理滚动效果的工具类   用mScroller记录/计算View滚动的位置,
     * 再重写View的computeScroll(),完成实际的滚动
     */
    private val mScroller: Scroller = Scroller(context)

    /**
     * 启动 fling 的滑动最小速率;
     */
    private val mMinVelocity: Int = ViewConfiguration.get(context).scaledMinimumFlingVelocity

    /**
     * 惯性滑动速度追踪类
     */
    private var mVelocityTracker: VelocityTracker? = null

    private var mLastX = 0      // 滑动初始 按下坐标值
    private var mMove: Int = 0  // 滑动X轴 偏移量;

    companion object{
        const val TAG = "MyRulerView"
    }

    init {
        // 初始化 Paint
        mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
            it.textSize = mTextSize
            it.color = mTextColor

            // 获取文字高度
            val fm = it.fontMetrics
            mTextHeight = fm.descent - fm.ascent
        }

        mScalePaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
            it.strokeWidth = mScaleWidth
            it.color = mTextColor
        }

        mCenterScalePaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
            it.color = mCenterColor
            it.strokeCap = Paint.Cap.ROUND
        }
    }

    /**
     * 设置标尺参数. 在未设置之前, View将会显示空白
     * @param currentValue  默认值
     * @param minValue      最大数值 (标尺上限)
     * @param maxValue      最小的数值 (标尺下限)
     * @param per           标尺精度  如 1:表示 每2条刻度差为1; 0.1:表示 每2条刻度差为0.1
     */
    fun setValue(currentValue: Float, minValue: Float, maxValue: Float, per: Float) {
        mCurrentValue = currentValue
        mMaxValue = maxValue
        mMinValue = minValue
        mPerValue = per * 10.0f

        // 计算总刻度数. 两头都有线,所以+1
        mTotalScale = ((mMaxValue - mMinValue) * 10 / mPerValue + 1).toInt()

        // 刻度总长度, 负数
        mMaxOffset = (-(mTotalScale - 1) * mScaleSpace).toInt()

        // 算当前位置. 也是负数
        mOffset = (mMinValue - mCurrentValue) / mPerValue * mScaleSpace * 10
        Log.d(TAG, "mOffset--" + mOffset + "=====mMaxOffset" + mMaxOffset + "mTotalLine" + mTotalScale)
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val heithtSpecSize = MeasureSpec.getSize(heightMeasureSpec)
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            // 这里 mScaleHeight * 2 是因为分为长线和短线. 亮色线最长为 2倍 基础长度;
            val height = paddingBottom + paddingTop + mTextHeight + mTextDistance + mScaleHeight * 2
            setMeasuredDimension(widthSpecSize, height.toInt())
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, heithtSpecSize)
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            val height = paddingBottom + paddingTop + mTextHeight + mTextDistance + mScaleHeight * 2
            setMeasuredDimension(widthSpecSize, height.toInt())
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        if (w > 0 && h > 0) {
            mTextLoc = h - mScaleHeight * 2 - mTextDistance - paddingBottom
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        var xInView: Float          // 刻度 在View中的理论位置; (如果超出屏幕, 则不需要去绘制)
        var realScaleheigh: Float   // 真实的刻度长度; 中间亮色部分 长度越大;
        var value: String           // 刻度值 的数值文字
        val halfWidth = width / 2   // 一半 View 的宽度. 也就是: 当前选中刻度的位置
        for (i in 0 until mTotalScale) {
            xInView = halfWidth + mOffset + i * mScaleSpace
            if (xInView < paddingStart || xInView > (width - paddingEnd)) {
                // 超出 View 外的刻度线. 就不画了
                continue
            }
            val dis = abs(xInView - halfWidth)
            if (dis <= mScaleSpace * 3) {
                // 当刻度距离中间较近时, 绘制量色刻度线. 计算刻度的长度 及 刻度的粗细;
                val rate = 1 - dis / (mScaleSpace * 3)
                realScaleheigh = (1.1f + rate * 0.5f) * mScaleHeight
                mCenterScalePaint.strokeWidth = mScaleWidth * (1.5f * rate + 1.5f)

                // 绘制刻度线
                canvas.drawLine(xInView, (height - paddingBottom).toFloat(), xInView,
                    height - paddingBottom - realScaleheigh,
                    mCenterScalePaint
                )
            } else {
                // 当刻度超出中间值过多时, 绘制暗色刻度线
                realScaleheigh = mScaleHeight
                canvas.drawLine(xInView, (height - paddingBottom).toFloat(), xInView,
                    height - paddingBottom - realScaleheigh,
                    mScalePaint
                )
            }
            if (i % 10 == 0) {
                // 整数时 绘制 刻度值
                value = (mMinValue + i * mPerValue / 10).toInt().toString()
                canvas.drawText(
                    value, xInView - mTextPaint.measureText(value) / 2,
                    mTextLoc, mTextPaint
                )
            }
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        Log.d(TAG, "onTouchEvent-")
        val action = event.action
        val xPosition = event.x.toInt()
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain()
        }
        mVelocityTracker!!.addMovement(event)
        when (action) {
            MotionEvent.ACTION_DOWN -> {
                mScroller.forceFinished(true)
                mLastX = xPosition
                mMove = 0
            }
            MotionEvent.ACTION_MOVE -> {
                // 计算移动值, 让标尺跟随移动
                mMove = mLastX - xPosition
                changeMoveAndValue()
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                // 根据滑动速率, 判断 启动 Scroller 的惯性滑动;
                countVelocityTracker()
                return false
            }
        }
        mLastX = xPosition
        return true
    }

    /**
     * 根据滑动速率, 启动惯性滑动
     */
    private fun countVelocityTracker() {
        Log.d(TAG, "countVelocityTracker-")
        mVelocityTracker!!.computeCurrentVelocity(1000) //初始化速率的单位
        val xVelocity = mVelocityTracker!!.xVelocity //当前的速度
        if (abs(xVelocity) > mMinVelocity) {
            mScroller.fling(0, 0,
                xVelocity.toInt(), 0, Int.MIN_VALUE, Int.MAX_VALUE, 0, 0
            )
        } else {
            countMoveEnd()
        }
    }

    /**
     * 滑动结束后,若是指针在2条刻度之间时,改变mOffset 让指针正好在刻度上。
     */
    private fun countMoveEnd() {
        mOffset -= mMove.toFloat()
        if (mOffset <= mMaxOffset) {
            mOffset = mMaxOffset.toFloat()
        } else if (mOffset >= 0) {
            mOffset = 0f
        }
        mLastX = 0
        mMove = 0
        mCurrentValue =
            mMinValue + (abs(mOffset) / mScaleSpace).roundToInt() * mPerValue / 10.0f
        mOffset = (mMinValue - mCurrentValue) * 10.0f / mPerValue * mScaleSpace
        notifyValueChange()
        postInvalidate()
    }

    /**
     * 滑动后的操作
     */
    private fun changeMoveAndValue() {
        mOffset -= mMove.toFloat()
        if (mOffset <= mMaxOffset) {
            mOffset = mMaxOffset.toFloat()
            mMove = 0
            mScroller.forceFinished(true)
        } else if (mOffset >= 0) {
            mOffset = 0f
            mMove = 0
            mScroller.forceFinished(true)
        }
        mCurrentValue =
            mMinValue + (abs(mOffset) / mScaleSpace).roundToInt() * mPerValue / 10.0f
        notifyValueChange()
        postInvalidate()
    }

    private fun notifyValueChange() {
        mVlaueListener?.invoke(mCurrentValue)
    }

    override fun computeScroll() {
        Log.d(TAG, "computeScroll-")
        //mScroller.computeScrollOffset()返回 true表示滑动还没有结束
        if (mScroller.computeScrollOffset()) {
            if (mScroller.currX == mScroller.finalX) {
                countMoveEnd()
            } else {
                val xPosition = mScroller.currX
                mMove = mLastX - xPosition
                changeMoveAndValue()
                mLastX = xPosition
            }
        }
    }
}

2.布局及Activity的代码

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".test.customview.RulerViewActivity">
        <TextView
            android:id="@+id/tv_one"
            style="@style/tv_base_16_dark"
            android:textSize="18sp"
            android:textStyle="bold"
            android:text="体重:"
            android:layout_marginStart="16dp"
            android:layout_marginTop="20dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <com.example.kotlinmvpframe.test.customview.custom.MyRulerView
            android:id="@+id/mrv_ruler"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="18dp"
            android:layout_marginTop="16dp"
            android:paddingHorizontal="12dp"
            android:paddingVertical="8dp"
            android:background="@drawable/shape_frame_green"
            app:layout_constraintTop_toBottomOf="@id/tv_one"
            app:layout_constraintStart_toStartOf="parent" />
        <TextView
            android:id="@+id/tv_weight"
            style="@style/tv_base_16_dark"
            android:textSize="18sp"
            android:textStyle="bold"
            android:textColor="@color/shape_green"
            android:text="0"
            android:layout_marginTop="12dp"
            app:layout_constraintTop_toBottomOf="@id/mrv_ruler"
            app:layout_constraintStart_toStartOf="@id/tv_one"/>
        <TextView
            style="@style/tv_base_16_dark"
            android:textColor="@color/shape_green"
            android:text="Kg"
            android:layout_marginStart="2dp"
            app:layout_constraintBaseline_toBaselineOf="@id/tv_weight"
            app:layout_constraintStart_toEndOf="@id/tv_weight"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="10dp"
            android:background="#cccccc"
            android:layout_marginTop="12dp"
            app:layout_constraintTop_toBottomOf="@id/tv_weight"/>

        <TextView
            android:id="@+id/tv_two"
            style="@style/tv_base_16_dark"
            android:textSize="18sp"
            android:textStyle="bold"
            android:text="身高:"
            android:layout_marginStart="16dp"
            android:layout_marginTop="36dp"
            app:layout_constraintTop_toBottomOf="@id/tv_weight"
            app:layout_constraintStart_toStartOf="parent" />
        <TextView
            android:id="@+id/tv_height"
            style="@style/tv_base_16_dark"
            android:textSize="18sp"
            android:textStyle="bold"
            android:textColor="@color/shape_green"
            android:text="0"
            android:layout_marginStart="6dp"
            app:layout_constraintBaseline_toBaselineOf="@id/tv_two"
            app:layout_constraintStart_toEndOf="@id/tv_two"/>
        <TextView
            style="@style/tv_base_16_dark"
            android:textColor="@color/shape_green"
            android:text="cm"
            android:layout_marginStart="2dp"
            app:layout_constraintBaseline_toBaselineOf="@id/tv_height"
            app:layout_constraintStart_toEndOf="@id/tv_height"/>
        <com.example.kotlinmvpframe.test.customview.custom.MyVerticalRulerView
            android:id="@+id/mrv_height"
            android:layout_width="wrap_content"
            android:layout_height="200dp"
            android:layout_marginStart="100dp"
            android:background="@drawable/shape_frame_green"
            app:layout_constraintTop_toTopOf="@id/tv_two"
            app:layout_constraintStart_toEndOf="@id/tv_two" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Activity:

// 体重标尺
binding.mrvRuler.mVlaueListener = {
    binding.tvWeight.text = it.toString()
}
binding.mrvRuler.setValue(60f, 0f, 120f, 0.1f)

// 身高标尺
binding.mrvHeight.setOnValueChangeListener { value ->
    binding.tvHeight.text = value.toString()
}
binding.mrvHeight.setValue(160f, 60f, 240f, 1f)

总结

没有总结

上一篇: 记一次自定义View: 扇形圆环
下一篇: 记一次自定义View:拖动小球图表

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值