一、效果图
二、自定义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()
}
}
}