Android自定义View之滑动选择身高、体重控件——MyRulerView

 一、效果图

二、自定义View基本步骤

onMeasure()测量:决定View的大小

onLayout()布局:决定View在ViewGroup中的位置

onDraw()绘制:决定绘制这个View,重写onDraw这个方法里对视图进行绘制,然后通过调用View的draw()方法来执行具体的绘制工作。

三、附完整代码

1、身高标尺需用到多个属性,res/values下创建一个attrs.xml文件,添加View需要用到的自定义属性,attrs.xml内容如下

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyRulerView">
        <attr name="textColor" format="color" />
        <attr name="textSize" format="dimension" />
        <attr name="lineColor" format="color" />
        <attr name="lineSpaceWidth" format="dimension" />
        <attr name="lineWidth" format="dimension" />
        <attr name="lineMaxHeight" format="dimension" />
        <attr name="lineMidHeight" format="dimension" />
        <attr name="lineMinHeight" format="dimension" />
        <attr name="textMarginTop" format="dimension" />
        <attr name="minValue" format="float"/>
        <attr name="maxValue" format="float"/>
        <attr name="selectorValue" format="float"/>
        <attr name="perValue" format="float"/>
    </declare-styleable>
</resources>

2、在drawble中新建bg_dialog.xml,用于设置标尺弹窗的背景。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <stroke
        android:width="0.5dp"
        android:color="#FFFFFFFF" />
    <corners
        android:topLeftRadius="8dp"
        android:topRightRadius="8dp" />
    <solid android:color="#FFFFFFFF" />
</shape>

3、dialog_height_ruler.xml完整代码,根据需要传入MyRulerView中app:对应的属性

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg_dialog"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        android:visibility="visible">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="left"
            android:layout_marginLeft="20dp"
            android:layout_marginTop="21dp"
            android:layout_marginRight="20dp">

            <ImageView
                android:id="@+id/close_image"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="left"
                android:src="@mipmap/icon_close" />

            <TextView
                android:id="@+id/ruler_title"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="right|center_vertical"
                android:text="选择身高"
                android:textColor="#333333"
                android:textSize="18sp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/required"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="right|center_vertical"
                android:text="确定"
                android:textColor="#FF00C0C5"
                android:textSize="16sp" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/discirble"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="16dp">

            <TextView
                android:id="@+id/tv_register_info_height_value"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="right"
                android:includeFontPadding="false"
                android:text="165"
                android:textColor="#FF00C0C5"
                android:textSize="32dp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/danwei"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="center_vertical|left"
                android:includeFontPadding="false"
                android:text="  厘米"
                android:textColor="#FF00C0C5"
                android:textSize="16dp" />
        </LinearLayout>

        <RelativeLayout
            android:layout_width="fill_parent"
            android:layout_height="116dp"
            android:layout_marginTop="24dp"
            android:background="#FFF5F6F7">
            <View
                android:layout_width="2dp"
                android:layout_height="95dp"
                android:layout_centerHorizontal="true"
                android:layout_marginBottom="19dp"
                android:background="#FF00C0C5" />

            <com.example.myrulerview.MyRulerView
                android:id="@+id/ruler_height"
                android:layout_width="match_parent"
                android:layout_height="116dp"
                android:background="@color/transparent"
                app:lineColor="#801d2129"
                app:lineMaxHeight="40dp"
                app:lineMidHeight="30dp"
                app:lineMinHeight="20dp"
                app:lineSpaceWidth="10dp"
                app:lineWidth="1dp"
                app:maxValue="250.0"
                app:minValue="80.0"
                app:perValue="1"
                app:selectorValue="165.0"
                app:textColor="@color/black" />

        </RelativeLayout>
    </LinearLayout>
</LinearLayout>

4、HeightDialog背景弹窗,使用dimAmount属性来调整变暗的程度(1.0不透明,0.0完全透明)。

package com.example.myrulerview

import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.Display
import android.view.Gravity
import android.view.WindowManager
import com.example.myrulerview.databinding.DialogHeightRulerBinding

@Suppress("DEPRECATION")
class HeightDialog(context: Context) : Dialog(context, R.style.UIAlertViewStyle) {
    private lateinit var mBinding: DialogHeightRulerBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DialogHeightRulerBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        //点击弹窗外侧关闭弹窗
        setCanceledOnTouchOutside(true)
        setCancelable(true)

        val windowManager: WindowManager? = window?.windowManager
        val lp: WindowManager.LayoutParams? = window?.attributes
        //所有在这个window之后的会变暗,使用dimAmount属性来控制变暗的程度(1.0不透明,0.0完全透明)
        lp?.alpha = 1f
        lp?.dimAmount = 0.5f
        window?.attributes = lp
        window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
        //设置窗口的占比
        val display: Display? = windowManager?.defaultDisplay
        (display?.height)?.div(2.2)
            ?.let { window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, it.toInt()) }
        //设置弹窗位置于屏幕底部
        window?.attributes?.gravity = Gravity.BOTTOM


        mBinding.rulerHeight.setTextChangedListener {
            //得到身高的最终值
            mBinding.tvRegisterInfoHeightValue.text = it.toString()
        }

        //关闭
        mBinding.closeImage.setOnClickListener {
            this.dismiss()
        }
        //确定
        mBinding.required.setOnClickListener {
            this.dismiss()
        }

    }
}

5、创建MyRulerView类,继承View,在init得到自定义属性,重写onDraw方法绘制刻度线以及下方数字。

package com.example.myrulerview

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.ViewConfiguration
import android.widget.Scroller

@SuppressLint("CustomViewStyleable")
class MyRulerView(context: Context, attrs: AttributeSet) : View(context, attrs) {

    private var call:((String)->Unit)? = null


    private var mLinePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)    //刻度画笔
    private var mTextPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)    //文字画笔

    private var mWidth: Int = 0
    private var mHeight: Int = 0

    //标尺
    private var mMaxValue = 250f           //最大值
    private var mMinValue = 80f            //最小值
    private var mPerValue = 1f             //最小刻度值,最小单位
    private var mLineSpace = 5f            //两条刻度之间的间隔距离

    private var mTotalLine = 0             //计算mMaxValue-mMinValue之间一共有多少条刻度线

    private var mMaxOffset = 0             //所有刻度共有多长 (mTotalLine-1)* mLineSpaceWidth

    private var mOffset = 0f               // 默认状态下,mSelectorValue所在的位置  位于尺子总刻度的位置

    private var mLastX: Int = 0
    private var mMove: Int = 0


    //刻度线
    private var mLineMaxLength = 40f       //三种不同长度(如刻度80cm-250cm),最长的那根线(80,90,100,...)时的线高度
    private var mLineMidLength = 30f       //中等长度(85,95,105,...)时的线高度
    private var mLineMinLength = 20f       //最短长度(81,82,83,...)时的线高度
    private var mLineWidth = 1f            //刻度线的粗细

    private var mLineColor = context.getColor(R.color.white6)    //刻度线颜色

    private var mSelectorValue = 100.0f                          // 未选择时 默认的值 指针指向的默认值


    //标尺下方文字
    private var mTextColor = context.getColor(R.color.black)       //文字颜色
    private var mTextSize = 35f                                    //文字大小
    private var mTextMarginTop = 10f                               //文字与上方的距离
    private var mTextHeight = 40f                                  //尺子刻度下方数字的高度

    private var mMinVelocity = 0


    private var mScroller: Scroller? = null
    private var mVelocityTracker: VelocityTracker? = null

    init {
        this.mLineSpace = myFloat(mLineSpace)
        this.mLineWidth = myFloat(mLineWidth)
        this.mLineMidLength = myFloat(mLineMidLength)
        this.mLineMinLength = myFloat(mLineMinLength)
        this.mTextHeight = myFloat(mTextHeight)

        mScroller = Scroller(context)

        val styleable = context.obtainStyledAttributes(attrs, R.styleable.MyRulerView)

        mMaxValue = styleable.getFloat(R.styleable.MyRulerView_maxValue, mMaxValue)
        mMinValue = styleable.getFloat(R.styleable.MyRulerView_minValue, mMinValue)
        mPerValue = styleable.getFloat(R.styleable.MyRulerView_perValue, mPerValue)
        mLineSpace = styleable.getDimension(R.styleable.MyRulerView_lineSpaceWidth, mLineSpace)
        mSelectorValue = styleable.getFloat(R.styleable.MyRulerView_selectorValue, 0f)

        mLineMaxLength = styleable.getDimension(R.styleable.MyRulerView_lineMaxHeight, mLineMaxLength)
        mLineMidLength =  styleable.getDimension(R.styleable.MyRulerView_lineMidHeight, mLineMidLength)
        mLineMinLength = styleable.getDimension(R.styleable.MyRulerView_lineMinHeight, mLineMinLength)
        mLineWidth = styleable.getDimension(R.styleable.MyRulerView_lineWidth, mLineWidth)

        mLineColor = styleable.getColor(R.styleable.MyRulerView_lineColor, mLineColor)


        mTextColor = styleable.getColor(R.styleable.MyRulerView_textColor, mTextColor)
        mTextSize = styleable.getDimension(R.styleable.MyRulerView_textSize, mTextSize)
        mTextMarginTop = styleable.getDimension(R.styleable.MyRulerView_textMarginTop, mTextMarginTop)

        styleable.recycle()

        mMinVelocity = ViewConfiguration.get(getContext()).scaledMinimumFlingVelocity
        initPaint()
        setRulerValue(mSelectorValue, mMaxValue,mMinValue , mPerValue)

    }

    fun myFloat(paramFloat: Float) = 0.5f + paramFloat * 1.0f

    /**
     * 初始化刻度线画笔、标尺下方文字画笔
     */
    private fun initPaint() {
        mTextPaint.color = mTextColor
        mTextPaint.textSize = mTextSize
        mTextPaint.typeface = Typeface.DEFAULT_BOLD

        mLinePaint.color = mLineColor
        mLinePaint.strokeWidth = mLineWidth
    }

    /**
     * 设置标尺的值
     */
    private fun setRulerValue(
        selectorValue: Float,
        maxValue: Float,
        minValue: Float,
        preValue: Float
    ) {
        Log.d("mSelectorValue---", mSelectorValue.toString())
        mSelectorValue = selectorValue
        mMaxValue = maxValue
        mMinValue = minValue
        mPerValue = preValue * 10f
        mTotalLine =
            ((mMaxValue * 10 - mMinValue * 10) / mPerValue).toInt() + 1  //需要画 mTotalLine 条刻度线
        mMaxOffset =
            (-(mTotalLine - 1) * mLineSpace).toInt()         //mTotalLine条刻度线之间有 mTotalLine-1 个间距
        mOffset = (mMinValue - mSelectorValue) / mPerValue * mLineSpace * 10
        invalidate()
        visibility = VISIBLE

    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        if (w > 0 && h > 0) {
            mWidth = w
            mHeight = h
        }
    }

    /**
     * 绘制刻度线
     */
    override fun onDraw(canvas: Canvas?) {
        var left: Float
        var value: String
        var height: Float
        val srcPointX = mWidth / 2
        super.onDraw(canvas)
        for (i in 0 until mTotalLine) {
            left = srcPointX + mOffset + i * mLineSpace
            if (left < 0 || left > width) {
                continue
            }
            //整10时,更改绘制时线的高度
            if (i % 10 == 0) {
                height = mLineMaxLength
                value = (mMinValue + i * mPerValue / 10).toInt().toString()
                mLinePaint.color = context.getColor(R.color.white6)
                //绘制刻度线下方数字
                canvas?.drawText(value, left - mTextPaint.measureText(value) / 2, height + mTextMarginTop + mTextHeight, mTextPaint)
                
            } else if (i % 5 == 0) {
                height = mLineMidLength
                mLinePaint.color = context.getColor(R.color.white5)
            } else {
                height = mLineMinLength
                mLinePaint.color = context.getColor(R.color.white5)
            }
            //画刻度线
            canvas?.drawLine(left, 0f, left, height, mLinePaint)

        }
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        var xPosition = event?.x?.toInt()
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain()
        }

        mVelocityTracker?.addMovement(event)

        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                mScroller?.forceFinished(true)
                mLastX = xPosition!!
                mMove = 0
            }
            MotionEvent.ACTION_MOVE -> {
                mMove = mLastX - xPosition!!
                changeMoveAndValue()
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                countMoveEnd()
                countVelocityTracker()
                return false

            }
        }
        mLastX = xPosition!!
        return true
    }

    /**
     * 滑动完成后如果指针落在两条刻度中间,则指向靠近的那条指针
     */
    private fun countMoveEnd() {
        mOffset -= mMove.toFloat()
        if (mOffset <= mMaxOffset) {
            mOffset = mMaxOffset.toFloat()
        } else if (mOffset >= 0) {
            mOffset = 0f
        }
        mLastX = 0
        mMove = 0
        mSelectorValue =
            mMinValue + Math.round(Math.abs(mOffset) * 1.0f / mLineSpace) * mPerValue / 10.0f
        mOffset = (mMinValue - mSelectorValue) * 10.0f / mPerValue * mLineSpace

        call?.invoke(mSelectorValue.toInt().toString())
        postInvalidate()
    }

    private fun countVelocityTracker() {
        mVelocityTracker?.computeCurrentVelocity(1000) //初始化速率的单位
        val xVelocity = mVelocityTracker!!.getXVelocity() //当前的速度
        if (Math.abs(xVelocity) > mMinVelocity) {
            mScroller!!.fling(0, 0, xVelocity.toInt(), 0, Int.MIN_VALUE, Int.MAX_VALUE, 0, 0)
        }
    }


    /**
     * 滑动后的操作
     */
    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)
        }
        mSelectorValue =
            mMinValue + Math.round(Math.abs(mOffset) * 1.0f / mLineSpace) * mPerValue / 10.0f
        call?.invoke(mSelectorValue.toInt().toString())
        postInvalidate()
    }

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

    fun setTextChangedListener(call: (String) -> Unit) {
        this.call = call
    }
}

6、MainActivty中调用身高弹窗。

package com.example.myrulerview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.myrulerview.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var mBinding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        mBinding.button.setOnClickListener {
            HeightDialog(this).show()
        }
    }
}

参考资料:Android自定义标尺控件(选择身高、体重等) - 简书

源码下载

猜你喜欢

转载自juejin.im/post/7229615985808572473
今日推荐