1. Rendering
2. Basic steps to customize View
onMeasure() measurement: determine the size of the View
onLayout() layout: determine the position of the View in the ViewGroup
onDraw() drawing: decide to draw this View, rewrite the onDraw method to draw the view, and then perform specific drawing work by calling the View's draw() method.
Three, with complete code
1. The height scale needs to use multiple attributes. Create an attrs.xml file under res/values, and add the custom attributes needed for View. The content of attrs.xml is as follows
<?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. Create a new bg_dialog.xml in drawble to set the background of the ruler pop-up window.
<?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. The complete code of dialog_height_ruler.xml is passed to the app in MyRulerView as needed: the corresponding attribute
<?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. For the HeightDialog background pop-up window, use the dimAmount attribute to adjust the degree of dimming (1.0 is opaque, 0.0 is completely transparent).
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. Create the MyRulerView class, inherit View, get custom attributes in init, and rewrite the onDraw method to draw the scale line and the numbers below.
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. Call the height pop-up window in 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()
}
}
}
Reference materials: Android custom ruler control (select height, weight, etc.) - Short Book