安卓 车轮视图 WheelView kotlin
安卓 车轮视图 WheelView kotlin
- 前言
- 一、代码解析
- 1.初始化
- 2.初始化数据
- 3.onMeasure
- 4.onDraw
- 5.onTouchEvent
- 6.其他
- 6.ItemObject
- 二、完整代码
- 总结
前言
有个需求涉及到类似这个视图,于是在网上找了个轮子,自己改吧改吧用,拿来主义当然后,但做事不仅要知其然,还要知其所以然,所以拿来用的同时还要理解。于是就有了本文。
一、代码解析
参数
/*** 控件宽度*/private var controlWidth = 0f/*** 控件高度*/private var controlHeight = 0f/*** 是否正在滑动** @return*//*** 是否滑动中*/var isScrolling = false/*** 选择的内容*/private val itemList: MutableList<ItemObject>? = mutableListOf()/*** 设置数据*/private var dataList = mutableListOf<String>()/*** 按下的坐标*/private var downY = 0/*** 按下的时间*/private var downTime: Long = 0/*** 短促移动*/private val goonTime: Long = 200/*** 短促移动距离*/private val goonDistance = 100/*** 画线画笔*/private var linePaint: Paint? = null/*** 线的默认颜色*/private var lineColor = -0x1000000/*** 线的默认宽度*/private var lineHeight = 2f/*** 默认字体*/private var normalFont = 14.0f/*** 选中的时候字体*/private var selectedFont = 22.0f/*** 单元格高度*/private var unitHeight = 50/*** 显示多少个内容*/private var itemNumber = 7/*** 默认字体颜色*/private var normalColor = -0x1000000/*** 选中时候的字体颜色*/private var selectedColor = -0x10000/*** 蒙板高度*/private var maskHeight = 48.0f/*** 选择监听*/private var onSelectListener: OnSelectListener? = null/*** 设置是否可用** @param isEnable*/var isEnable = true/*** 是否允许选空*/private var noEmpty = true/*** 正在修改数据,避免ConcurrentModificationException异常*/private var isClearing = false
1.初始化
/*** 初始化,获取设置的属性** @param context* @param attrs*/private fun init(context: Context, attrs: AttributeSet?) {val attribute = context.obtainStyledAttributes(attrs, R.styleable.WheelView)unitHeight =attribute.getDimension(R.styleable.WheelView_unitHeight, unitHeight.toFloat()).toInt()itemNumber = attribute.getInt(R.styleable.WheelView_itemNumber, itemNumber)normalFont = attribute.getDimension(R.styleable.WheelView_normalTextSize, normalFont)selectedFont = attribute.getDimension(R.styleable.WheelView_selectedTextSize, selectedFont)normalColor = attribute.getColor(R.styleable.WheelView_normalTextColor, normalColor)selectedColor = attribute.getColor(R.styleable.WheelView_selectedTextColor, selectedColor)lineColor = attribute.getColor(R.styleable.WheelView_lineColor, lineColor)lineHeight = attribute.getDimension(R.styleable.WheelView_lineHeight, lineHeight)maskHeight = attribute.getDimension(R.styleable.WheelView_maskHeight, maskHeight)noEmpty = attribute.getBoolean(R.styleable.WheelView_noEmpty, true)isEnable = attribute.getBoolean(R.styleable.WheelView_isEnable, true)attribute.recycle()controlHeight = (itemNumber * unitHeight).toFloat()}
上面的代码在构造函数中调用,通过 context.obtainStyledAttributes(attrs, R.styleable.WheelView) 方法拿到我们在attrs.xml文件中自定义样式的一些参数。这些参数是可以在xml中配置的。这些都是自定义view的一些基础,属于老生常谈了。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="WheelView"><attr name="unitHeight" format="dimension" /><attr name="itemNumber" format="integer"/><attr name="normalTextColor" format="color" /><attr name="normalTextSize" format="dimension" /><attr name="selectedTextColor" format="color" /><attr name="selectedTextSize" format="dimension" /><attr name="lineColor" format="color" /><attr name="lineHeight" format="dimension" /><attr name="maskHeight" format="dimension"/><attr name="noEmpty" format="boolean"/><attr name="isEnable" format="boolean"/></declare-styleable></resources>
2.初始化数据
/*** 初始化数据*/private fun initData() {isClearing = trueitemList!!.clear()for (i in dataList.indices) {val itemObject = ItemObject()itemObject.id = iitemObject.itemText = dataList[i]itemObject.x = 0itemObject.y = i * unitHeightitemList.add(itemObject)}isClearing = false}
这里就是初始化item数据,ItemObject是单个数据的绘制,后面再说。而isClearing 是为了避免 ConcurrentModificationException,在drawList()方法中有体现。
@Synchronizedprivate fun drawList(canvas: Canvas) {if (isClearing) returntry {for (itemObject in itemList!!) {itemObject.drawSelf(canvas, measuredWidth)}} catch (e: Exception) {Log.e("WheelView", "drawList: $e")}}
3.onMeasure
自定义view的三件套之一
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)controlWidth = measuredWidth.toFloat()if (controlWidth != 0f) {setMeasuredDimension(measuredWidth, itemNumber * unitHeight)}}
先用measuredWidth给 controlWidth 赋值 ,然后当宽度不为0的时候调用setMeasuredDimension方法给具体的测量值。我们来看看setMeasuredDimension方法
这是一个view的自带方法,onMeasure(int,int)必须调用此方法来存储测量的宽度和测量的高度。否则将在测量时触发异常。
参数:
- measuredWidth–此视图的测量宽度。可以是MEASURED_SIZE_mask和MEASURED_STATE_TOO_SMALL定义的复杂位掩码。
- measuredHeight–此视图的测量高度。可以是MEASURED_SIZE_mask和MEASURED_STATE_TOO_SMALL定义的复杂位掩码。
4.onDraw
override fun onDraw(canvas: Canvas) {super.onDraw(canvas)drawLine(canvas)drawList(canvas)drawMask(canvas)}
在其中绘制了三个东西,一个是绘制选中的两个线条
/*** 绘制线条** @param canvas*/private fun drawLine(canvas: Canvas) {if (linePaint == null) {linePaint = Paint()linePaint!!.color = lineColorlinePaint!!.isAntiAlias = truelinePaint!!.strokeWidth = lineHeight}canvas.drawLine(0f, controlHeight / 2 - unitHeight / 2 + lineHeight,controlWidth, controlHeight / 2 - unitHeight / 2 + lineHeight, linePaint!!)canvas.drawLine(0f, controlHeight / 2 + unitHeight / 2 - lineHeight,controlWidth, controlHeight / 2 + unitHeight / 2 - lineHeight, linePaint!!)}
一个是绘制列表,上面已经说过了,还有一个就是绘制蒙层,我这边是有一个渐变的蒙层,也是我做过改动的地方
/*** 绘制遮盖板** @param canvas*/private fun drawMask(canvas: Canvas) {val colorArray = intArrayOf(0x0042C8FF, 0x3D42C8FF, 0x0042C8FF)val positionArray = floatArrayOf(0f, 0.5f, 1f)val lg3 = LinearGradient(0f, 0f,controlWidth, 0f, colorArray, positionArray, TileMode.MIRROR)val paint3 = Paint()paint3.shader = lg3canvas.drawRect(0f, controlHeight / 2 - unitHeight / 2 + lineHeight, controlWidth,controlHeight / 2 + unitHeight / 2 - lineHeight, paint3)}
5.onTouchEvent
触摸事件
override fun onTouchEvent(event: MotionEvent): Boolean {if (!isEnable) return trueval y = event.y.toInt()when (event.action) {MotionEvent.ACTION_DOWN -> {isScrolling = truedownY = event.y.toInt()downTime = System.currentTimeMillis()}MotionEvent.ACTION_MOVE -> {actionMove(y - downY)onSelectListener()}MotionEvent.ACTION_UP -> {val move = Math.abs(y - downY)// 判断这段时间移动的距离if (System.currentTimeMillis() - downTime < goonTime && move > goonDistance) {goonMove(y - downY)} else {actionUp(y - downY)noEmpty()isScrolling = false}}else -> {}}return true}
代码解析:
isEnable是控制是否能滑动的,不用过多关注
在手势为ACTION_DOWN 的时候,记录开始滑动的y坐标和时间,在手势为**ACTION_MOVE **的时候开始移动,并调用actionMove方法设置移动的坐标,然后调用invalidate方法进行重绘。onSelectListener是一个滑动时候的选中监听
/*** 移动的时候** @param move*/private fun actionMove(move: Int) {for (item in itemList!!) {item.move(move)}invalidate()}
最后在手势为ACTION_UP 的时候,判断在ACTION_DOWN这段时间移动的距离,如果当前移动的时间小于短促移动的时间,当前移动的距离却大于短促移动的距离,那么我们就调用goonMove方法多移动一点距离以达到一个惯性移动的体验。
/*** 继续移动一定距离*/@Synchronizedprivate fun goonMove(move: Int) {Thread {var distance = 0while (distance < unitHeight * MOVE_NUMBER) {try {Thread.sleep(5)} catch (e: InterruptedException) {e.printStackTrace()}actionThreadMove(if (move > 0) distance else distance * -1)distance += 10}actionUp(if (move > 0) distance - 10 else distance * -1 + 10)noEmpty()}.start()}/*** 移动,线程中调用** @param move*/private fun actionThreadMove(move: Int) {for (item in itemList!!) {item.move(move)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)}
否则就直接用actionUp和noEmpty直接移动
/*** 松开的时候** @param move*/private fun actionUp(move: Int) {var newMove = 0if (move > 0) {for (i in itemList!!.indices) {if (itemList[i].isSelected) {newMove = itemList[i].moveToSelected().toInt()if (onSelectListener != null) onSelectListener!!.endSelect(itemList[i].id,itemList[i].itemText)break}}} else {for (i in itemList!!.indices.reversed()) {if (itemList[i].isSelected) {newMove = itemList[i].moveToSelected().toInt()if (onSelectListener != null) onSelectListener!!.endSelect(itemList[i].id,itemList[i].itemText)break}}}for (item in itemList) {item.newY(move + 0)}slowMove(newMove)val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)}/*** 不能为空,必须有选项*/private fun noEmpty() {if (!noEmpty) returnfor (item in itemList!!) {if (item.isSelected) return}val move = itemList[0].moveToSelected().toInt()if (move < 0) {defaultMove(move)} else {defaultMove(itemList[itemList.size - 1].moveToSelected().toInt())}for (item in itemList) {if (item.isSelected) {if (onSelectListener != null) onSelectListener!!.endSelect(item.id, item.itemText)break}}}
6.其他
/*** 缓慢移动** @param move*/@Synchronizedprivate fun slowMove(move: Int) {Thread { // 判断正负var m = if (move > 0) move else move * -1val i = if (move > 0) 1 else -1// 移动速度val speed = 1while (true) {m -= speedif (m <= 0) {for (item in itemList!!) {item.newY(m * i)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)try {Thread.sleep(2)} catch (e: InterruptedException) {e.printStackTrace()}break}for (item in itemList!!) {item.newY(speed * i)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)try {Thread.sleep(2)} catch (e: InterruptedException) {e.printStackTrace()}}if (itemList != null) {for (item in itemList) {if (item.isSelected) {if (onSelectListener != null) onSelectListener!!.endSelect(item.id,item.itemText)break}}}}.start()}/*** 移动到默认位置** @param move*/private fun defaultMove(move: Int) {for (item in itemList!!) {item.newY(move)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)}
一些移动相关的方法。
6.ItemObject
单个绘制里面值得说的就是drawSelf方法了,注释都写的比较清晰了。
/*** 绘制自身** @param canvas 画板* @param containerWidth 容器宽度*/fun drawSelf(canvas: Canvas, containerWidth: Int) {if (textPaint == null) {textPaint = TextPaint()textPaint!!.isAntiAlias = true}if (textRect == null) textRect = Rect()// 判断是否被选择if (isSelected) {textPaint!!.color = selectedColor// 获取距离标准位置的距离var moveToSelect = moveToSelected()moveToSelect = if (moveToSelect > 0) moveToSelect else moveToSelect * -1// 计算当前字体大小val textSize =normalFont + (selectedFont - normalFont) * (1.0f - moveToSelect / unitHeight.toFloat())textPaint!!.textSize = textSize} else {textPaint!!.color = normalColortextPaint!!.textSize = normalFont}// 判断是否可视if (!isInView) return//判断是一行还是两行,两行数据用,分割if (itemText.indexOf(",") != -1) {var (text1, text2) = itemText.split(",")// 返回包围整个字符串的最小的一个Rect区域text1 = TextUtils.ellipsize(text1,textPaint,containerWidth.toFloat(),TextUtils.TruncateAt.END) as StringtextPaint!!.getTextBounds(text1, 0, text1.length, textRect)//双排文字一canvas.drawText(text1,x + controlWidth / 2 - textRect!!.width() / 2,(y + move + unitHeight / 5 * 2 + textRect!!.height() / 5 * 2).toFloat(),textPaint!!)// 返回包围整个字符串的最小的一个Rect区域text2 = TextUtils.ellipsize(text2,textPaint,containerWidth.toFloat(),TextUtils.TruncateAt.END) as StringtextPaint!!.getTextBounds(text2, 0, text2.length, textRect)//双排文字2canvas.drawText(text2,x + controlWidth / 2 - textRect!!.width() / 2,(y + move + unitHeight / 5 * 3 + textRect!!.height() / 5 * 3).toFloat(),textPaint!!)} else {// 返回包围整个字符串的最小的一个Rect区域itemText = TextUtils.ellipsize(itemText,textPaint,containerWidth.toFloat(),TextUtils.TruncateAt.END) as StringtextPaint!!.getTextBounds(itemText, 0, itemText.length, textRect)// 绘制内容canvas.drawText(itemText, x + controlWidth / 2 - textRect!!.width() / 2,(y + move + unitHeight / 2 + textRect!!.height() / 2).toFloat(), textPaint!!)}}
二、完整代码
import android.content.Context
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Shader.TileMode
import android.os.Handler
import android.os.Message
import android.text.TextPaint
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.Viewclass WheelView : View {/*** 控件宽度*/private var controlWidth = 0f/*** 控件高度*/private var controlHeight = 0f/*** 是否正在滑动** @return*//*** 是否滑动中*/var isScrolling = false/*** 选择的内容*/private val itemList: MutableList<ItemObject>? = mutableListOf()/*** 设置数据*/private var dataList = mutableListOf<String>()/*** 按下的坐标*/private var downY = 0/*** 按下的时间*/private var downTime: Long = 0/*** 短促移动*/private val goonTime: Long = 200/*** 短促移动距离*/private val goonDistance = 100/*** 画线画笔*/private var linePaint: Paint? = null/*** 线的默认颜色*/private var lineColor = -0x1000000/*** 线的默认宽度*/private var lineHeight = 2f/*** 默认字体*/private var normalFont = 14.0f/*** 选中的时候字体*/private var selectedFont = 22.0f/*** 单元格高度*/private var unitHeight = 50/*** 显示多少个内容*/private var itemNumber = 7/*** 默认字体颜色*/private var normalColor = -0x1000000/*** 选中时候的字体颜色*/private var selectedColor = -0x10000/*** 蒙板高度*/private var maskHeight = 48.0f/*** 选择监听*/private var onSelectListener: OnSelectListener? = null/*** 设置是否可用** @param isEnable*/var isEnable = true/*** 是否允许选空*/private var noEmpty = true/*** 正在修改数据,避免ConcurrentModificationException异常*/private var isClearing = falseconstructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context,attrs,defStyle) {init(context, attrs)initData()}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {init(context, attrs)initData()}constructor(context: Context?) : super(context) {initData()}/*** 初始化,获取设置的属性** @param context* @param attrs*/private fun init(context: Context, attrs: AttributeSet?) {val attribute = context.obtainStyledAttributes(attrs, R.styleable.WheelView)unitHeight =attribute.getDimension(R.styleable.WheelView_unitHeight, unitHeight.toFloat()).toInt()itemNumber = attribute.getInt(R.styleable.WheelView_itemNumber, itemNumber)normalFont = attribute.getDimension(R.styleable.WheelView_normalTextSize, normalFont)selectedFont = attribute.getDimension(R.styleable.WheelView_selectedTextSize, selectedFont)normalColor = attribute.getColor(R.styleable.WheelView_normalTextColor, normalColor)selectedColor = attribute.getColor(R.styleable.WheelView_selectedTextColor, selectedColor)lineColor = attribute.getColor(R.styleable.WheelView_lineColor, lineColor)lineHeight = attribute.getDimension(R.styleable.WheelView_lineHeight, lineHeight)maskHeight = attribute.getDimension(R.styleable.WheelView_maskHeight, maskHeight)noEmpty = attribute.getBoolean(R.styleable.WheelView_noEmpty, true)isEnable = attribute.getBoolean(R.styleable.WheelView_isEnable, true)attribute.recycle()controlHeight = (itemNumber * unitHeight).toFloat()}/*** 初始化数据*/private fun initData() {isClearing = trueitemList!!.clear()for (i in dataList.indices) {val itemObject = ItemObject()itemObject.id = iitemObject.itemText = dataList[i]itemObject.x = 0itemObject.y = i * unitHeightitemList.add(itemObject)}isClearing = false}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)controlWidth = measuredWidth.toFloat()if (controlWidth != 0f) {setMeasuredDimension(measuredWidth, itemNumber * unitHeight)}}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)drawLine(canvas)drawList(canvas)drawMask(canvas)}/*** 绘制线条** @param canvas*/private fun drawLine(canvas: Canvas) {if (linePaint == null) {linePaint = Paint()linePaint!!.color = lineColorlinePaint!!.isAntiAlias = truelinePaint!!.strokeWidth = lineHeight}canvas.drawLine(0f, controlHeight / 2 - unitHeight / 2 + lineHeight,controlWidth, controlHeight / 2 - unitHeight / 2 + lineHeight, linePaint!!)canvas.drawLine(0f, controlHeight / 2 + unitHeight / 2 - lineHeight,controlWidth, controlHeight / 2 + unitHeight / 2 - lineHeight, linePaint!!)}@Synchronizedprivate fun drawList(canvas: Canvas) {if (isClearing) returntry {for (itemObject in itemList!!) {itemObject.drawSelf(canvas, measuredWidth)}} catch (e: Exception) {Log.e("WheelView", "drawList: $e")}}/*** 绘制遮盖板** @param canvas*/private fun drawMask(canvas: Canvas) {val colorArray = intArrayOf(0x0042C8FF, 0x3D42C8FF, 0x0042C8FF)val positionArray = floatArrayOf(0f, 0.5f, 1f)val lg3 = LinearGradient(0f, 0f,controlWidth, 0f, colorArray, positionArray, TileMode.MIRROR)val paint3 = Paint()paint3.shader = lg3canvas.drawRect(0f, controlHeight / 2 - unitHeight / 2 + lineHeight, controlWidth,controlHeight / 2 + unitHeight / 2 - lineHeight, paint3)}override fun onTouchEvent(event: MotionEvent): Boolean {if (!isEnable) return trueval y = event.y.toInt()when (event.action) {MotionEvent.ACTION_DOWN -> {isScrolling = truedownY = event.y.toInt()downTime = System.currentTimeMillis()}MotionEvent.ACTION_MOVE -> {actionMove(y - downY)onSelectListener()}MotionEvent.ACTION_UP -> {val move = Math.abs(y - downY)// 判断这段时间移动的距离if (System.currentTimeMillis() - downTime < goonTime && move > goonDistance) {goonMove(y - downY)} else {actionUp(y - downY)noEmpty()isScrolling = false}}else -> {}}return true}/*** 继续移动一定距离*/@Synchronizedprivate fun goonMove(move: Int) {Thread {var distance = 0while (distance < unitHeight * MOVE_NUMBER) {try {Thread.sleep(5)} catch (e: InterruptedException) {e.printStackTrace()}actionThreadMove(if (move > 0) distance else distance * -1)distance += 10}actionUp(if (move > 0) distance - 10 else distance * -1 + 10)noEmpty()}.start()}/*** 不能为空,必须有选项*/private fun noEmpty() {if (!noEmpty) returnfor (item in itemList!!) {if (item.isSelected) return}val move = itemList[0].moveToSelected().toInt()if (move < 0) {defaultMove(move)} else {defaultMove(itemList[itemList.size - 1].moveToSelected().toInt())}for (item in itemList) {if (item.isSelected) {if (onSelectListener != null) onSelectListener!!.endSelect(item.id, item.itemText)break}}}/*** 移动的时候** @param move*/private fun actionMove(move: Int) {for (item in itemList!!) {item.move(move)}invalidate()}/*** 移动,线程中调用** @param move*/private fun actionThreadMove(move: Int) {for (item in itemList!!) {item.move(move)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)}/*** 松开的时候** @param move*/private fun actionUp(move: Int) {var newMove = 0if (move > 0) {for (i in itemList!!.indices) {if (itemList[i].isSelected) {newMove = itemList[i].moveToSelected().toInt()if (onSelectListener != null) onSelectListener!!.endSelect(itemList[i].id,itemList[i].itemText)break}}} else {for (i in itemList!!.indices.reversed()) {if (itemList[i].isSelected) {newMove = itemList[i].moveToSelected().toInt()if (onSelectListener != null) onSelectListener!!.endSelect(itemList[i].id,itemList[i].itemText)break}}}for (item in itemList) {item.newY(move + 0)}slowMove(newMove)val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)}/*** 缓慢移动** @param move*/@Synchronizedprivate fun slowMove(move: Int) {Thread { // 判断正负var m = if (move > 0) move else move * -1val i = if (move > 0) 1 else -1// 移动速度val speed = 1while (true) {m -= speedif (m <= 0) {for (item in itemList!!) {item.newY(m * i)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)try {Thread.sleep(2)} catch (e: InterruptedException) {e.printStackTrace()}break}for (item in itemList!!) {item.newY(speed * i)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)try {Thread.sleep(2)} catch (e: InterruptedException) {e.printStackTrace()}}if (itemList != null) {for (item in itemList) {if (item.isSelected) {if (onSelectListener != null) onSelectListener!!.endSelect(item.id,item.itemText)break}}}}.start()}/*** 移动到默认位置** @param move*/private fun defaultMove(move: Int) {for (item in itemList!!) {item.newY(move)}val rMessage = Message()rMessage.what = REFRESH_VIEWmHandler.sendMessage(rMessage)}/*** 滑动监听*/private fun onSelectListener() {if (onSelectListener == null) returnfor (item in itemList!!) {if (item.isSelected) {onSelectListener!!.selecting(item.id, item.itemText)}}}/*** 设置数据 (第一次)** @param data*/fun setData(data: MutableList<String>) {dataList = datainitData()}/*** 重置数据** @param data*/fun refreshData(data: MutableList<String>) {setData(data)invalidate()}/*** 获取返回项 id** @return*/val selected: Intget() {for (item in itemList!!) {if (item.isSelected) return item.id}return -1}/*** 获取返回的内容** @return*/val selectedText: Stringget() {for (item in itemList!!) {if (item.isSelected) return item.itemText}return ""}/*** 设置默认选项** @param index*/fun setDefault(index: Int) {if (index > itemList!!.size - 1) returnval move = itemList[index].moveToSelected()defaultMove(move.toInt())}/*** 获取列表大小** @return*/val listSize: Intget() = itemList?.size ?: 0/*** 获取某项的内容** @param index* @return*/fun getItemText(index: Int): String {return itemList?.get(index)?.itemText ?: ""}/*** 监听** @param onSelectListener*/fun setOnSelectListener(onSelectListener: OnSelectListener?) {this.onSelectListener = onSelectListener}var mHandler: Handler =object : Handler() {override fun handleMessage(msg: Message) {super.handleMessage(msg)when (msg.what) {REFRESH_VIEW -> invalidate()else -> {}}}}/*** 单条内容*/private inner class ItemObject {/*** id*/var id = 0/*** 内容*/var itemText = ""/*** x坐标*/var x = 0/*** y坐标*/var y = 0/*** 移动距离*/var move = 0/*** 字体画笔*/private var textPaint: TextPaint? = null/*** 字体范围矩形*/private var textRect: Rect? = null/*** 绘制自身** @param canvas 画板* @param containerWidth 容器宽度*/fun drawSelf(canvas: Canvas, containerWidth: Int) {if (textPaint == null) {textPaint = TextPaint()textPaint!!.isAntiAlias = true}if (textRect == null) textRect = Rect()// 判断是否被选择if (isSelected) {textPaint!!.color = selectedColor// 获取距离标准位置的距离var moveToSelect = moveToSelected()moveToSelect = if (moveToSelect > 0) moveToSelect else moveToSelect * -1// 计算当前字体大小val textSize =normalFont + (selectedFont - normalFont) * (1.0f - moveToSelect / unitHeight.toFloat())textPaint!!.textSize = textSize} else {textPaint!!.color = normalColortextPaint!!.textSize = normalFont}// 判断是否可视if (!isInView) return//判断是一行还是两行,两行数据用,分割if (itemText.indexOf(",") != -1) {var (text1, text2) = itemText.split(",")// 返回包围整个字符串的最小的一个Rect区域text1 = TextUtils.ellipsize(text1,textPaint,containerWidth.toFloat(),TextUtils.TruncateAt.END) as StringtextPaint!!.getTextBounds(text1, 0, text1.length, textRect)//双排文字一canvas.drawText(text1,x + controlWidth / 2 - textRect!!.width() / 2,(y + move + unitHeight / 5 * 2 + textRect!!.height() / 5 * 2).toFloat(),textPaint!!)// 返回包围整个字符串的最小的一个Rect区域text2 = TextUtils.ellipsize(text2,textPaint,containerWidth.toFloat(),TextUtils.TruncateAt.END) as StringtextPaint!!.getTextBounds(text2, 0, text2.length, textRect)//双排文字2canvas.drawText(text2,x + controlWidth / 2 - textRect!!.width() / 2,(y + move + unitHeight / 5 * 3 + textRect!!.height() / 5 * 3).toFloat(),textPaint!!)} else {// 返回包围整个字符串的最小的一个Rect区域itemText = TextUtils.ellipsize(itemText,textPaint,containerWidth.toFloat(),TextUtils.TruncateAt.END) as StringtextPaint!!.getTextBounds(itemText, 0, itemText.length, textRect)// 绘制内容canvas.drawText(itemText, x + controlWidth / 2 - textRect!!.width() / 2,(y + move + unitHeight / 2 + textRect!!.height() / 2).toFloat(), textPaint!!)}}/*** 是否在可视界面内** @return*/val isInView: Booleanget() = if (y + move > controlHeight || y + move + unitHeight / 2 + textRect!!.height() / 2 < 0) false else true/*** 移动距离** @param _move*/fun move(_move: Int) {move = _move}/*** 设置新的坐标** @param _move*/fun newY(_move: Int) {move = 0y = y + _move}/*** 判断是否在选择区域内** @return*/val isSelected: Booleanget() {if (y + move + unitHeight >= controlHeight / 2 - unitHeight / 2 + lineHeight&& y + move + unitHeight <= controlHeight / 2 + unitHeight / 2 - lineHeight) {return true}return (y + move <= controlHeight / 2 - unitHeight / 2 + lineHeight&& y + move + unitHeight >= controlHeight / 2 + unitHeight / 2 - lineHeight)}/*** 获取移动到标准位置需要的距离*/fun moveToSelected(): Float {return controlHeight / 2 - unitHeight / 2 - (y + move)}}/*** 选择监听** @author JiangPing*/interface OnSelectListener {/*** 结束选择** @param id* @param text*/fun endSelect(id: Int, text: String?)/*** 选中的内容** @param id* @param text*/fun selecting(id: Int, text: String?)}companion object {/*** 刷新界面*/private const val REFRESH_VIEW = 0x001/*** 移动距离*/private const val MOVE_NUMBER = 5}
}
总结
主要还是考查自定义view相关能力。
相关文章:

安卓 车轮视图 WheelView kotlin
安卓 车轮视图 WheelView kotlin 前言一、代码解析1.初始化2.初始化数据3.onMeasure4.onDraw5.onTouchEvent6.其他 6.ItemObject二、完整代码总结 前言 有个需求涉及到类似这个视图,于是在网上找了个轮子,自己改吧改吧用,拿来主义当然后&…...
升级Redisson版本兼容问题
升级版本:从 3.10.6 升级到3.18.0 报错: java.io.IOException: Unsupported protocol version 252 java.io.IOException: Unsupported protocol version 252at org.jboss.marshalling.river.RiverUnmarshaller.start(RiverUnmarshaller.java:1375)at org.redisson…...
前端框架Bootstrap
前端框架Bootstrap 该框架已经帮我们写好了很多页面样式,如果需要使用,只需要下载对应文件 直接CV拷贝即可 在使用Bootstrap的时候,所有的页面样式只需要通过修改class属性来调节即可 什么是Bootstrap Bootstrap是一个开源的前端框架…...

Flink SQL TopN语句详解
TopN 定义(⽀持 Batch\Streaming): TopN 对应离线数仓的 row_number(),使⽤ row_number() 对某⼀个分组的数据进⾏排序。 应⽤场景: 根据 某个排序 条件,计算 某个分组 下的排⾏榜数据。 SQL 语法标准&am…...
k8s之数据卷
一,存储卷 容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃时,kubelet 会重启它,但是容器中的文件将丢失——容器以干净的状态(镜像最初的状态&#…...
服务器网络
配置 通常使用ping查看网络 如果能ping通,不能ssh登陆,安装 sudo apt update sudo apt install openssh-server如果已经安装,查看防火墙状态,inactive(不活跃) sudo ufw status sudo ufw allow ssh sudo ufw reload查看ssh状态 s…...

YOLOv8-seg 分割代码详解(一)Predict
前言 本文从 U-Net 入手熟悉分割的简单方法,再看 YOLOv8 的方法。主要梳理 YOLOv8 的网络结构,以及 Predict 过程的后处理方法。 U-Net 代码地址:https://github.com/milesial/Pytorch-UNet YOLOv8 代码地址:https://github.com/…...

Docker学习——④
文章目录 1、Docker Image(镜像)2、镜像命令详解2.1 docker rmi2.2 docker save2.3 docker load2.4 docker image inspect2.5 docker history2.6 docker image prune 3、镜像综合实战3.1 离线镜像迁移3.2 镜像存储的压缩与共享 1、Docker Imageÿ…...

Android选项卡TabHost
选项卡主要由TabHost(标签,主人),TabWidget(微件)和FrameLayout3个组件组成,用于实现一个多标签页的用户界面。 1. TabHost在XML文件中添加: XML布局文件中添加选项卡时必须使用系统id来为各组件指定id属性。 <TabHostandro…...

qml添加滚动条
import QtQuick.Controls 2.15ScrollBar.vertical: ScrollBar {visible: flick1.contentHeight > flick1.heightanchors.right: parent.rightanchors.rightMargin: 40width: 10active: truecontentItem: Rectangle {radius: 6opacity: 0.5color: "#7882A0"} }...

elementui-plus el-tree组件数据不显示问题解决
当前情况: 显示: 注意看右侧的树是没有文字的,数据已经渲染,个数是对的,但就是没有文字, 解决: 对比以后发现是template中的#default{data}没有写大括号导致的 所以写上大括号后: 正常显示...
EMR 磁盘挂载解读与磁盘扩容操作
云上的计算实例挂载的存储盘通常可以在线实现磁盘扩容。本文以 AWS EMR 节点的磁盘扩容为例,记录一下具体的操作步骤。在详细介绍前,先将重要的总结发在前面,便于以后查阅: EMR 磁盘分配规则是: 第一磁盘(/dev/nvme0n1),必备,大小由控制台的"EBS root volume&qu…...

小程序day04
目标 自定义组件 创建组件 引用组件 局部引用 全局引用 组件的函数定义到metods节点中,梦回vue2. 样式 数据,方法,属性 下划线开头的称为自定义方法,非下划线开头的都是事件处理函数。 神特么,this.datathis.pro…...
哪些人更容易受到网络攻击?
当下,企业的安全已从传统的外部网络安全威胁防御,逐渐延伸到内部威胁防御。很多时候IT基础设施被攻陷不是外部造成,而是内部使然,这些内部威胁要复杂得多且难以管理。那么,哪些员工最脆弱、最有可能给企业组织带来网络…...
sql语句-实体属性有集合怎么批量查询
1、背景 前端返回一个实体类,实体类里还有集合。要对集合外的属性查询,还要对集合批量查询,并且属性可能为空。返回给前端的结果是个实体类,实体类里有集合。 2、前端实体类 public class AppletSyncDiseaseInfoBO {// 病害信息…...

临界资源,临界区,通信的干扰问题(互斥),信号量(本质,上下文切换问题,原子性,自身的安全性,操作)
目录 引入 概念 临界资源 临界区 干扰存在原因 互斥 信号量 引入 举例 概念 介绍 表示可用资源数 表示等待进程数 申请信号量 信号量的本质 全局变量? 共享内存? 不安全问题 -- 上下文切换 原子性 信号量自身的安全性 原子操作的意义 操作 引入 通信…...

工具介绍——第三方软件远程连接(工具:Rustdesk)
文章目录 前言一、使用工具二、开始演示1、拿下目标主机权限后上传文件2、运行目标主机上的rustdesk-1.1.9.exe文件3、目标主机上whoami查看现在的用户4、查找目标主机上连接的文件,并添加连接密码5、目标主机重启rustdesk的应用程序6、本地连接主机 前言 这里主要…...

【脑机接口 算法】EEGNet: 通用神经网络应用于脑电信号
EEGNet: 神经网络应用于脑电信号 中文题目论文下载:算法程序下载:摘要1 项目介绍2 EEGNet网络原理2.1EEGNet原理架构2.2FBCCA 算法2.3自适应FBCCA算法 3EEGNet网络实现4结果 中文题目 论文下载: DOI: 算法程序下载: 地址 摘要…...

【会话技术】Cookie和Session的工作流程和区别
Cookie技术 web程序是通过HTTP协议传输的,而HTTP是无状态的,即后续如果还要使用前面已经传输的数据,就还需要重传。这样如果数据量很大的情况下,效率就会大打折扣。Cookie的出现就是为了解决这个问题。 Cookie的工作流程&#x…...

Xmake v2.8.5 发布,支持链接排序和单元测试
Xmake 是一个基于 Lua 的轻量级跨平台构建工具。 它非常的轻量,没有任何依赖,因为它内置了 Lua 运行时。 它使用 xmake.lua 维护项目构建,相比 makefile/CMakeLists.txt,配置语法更加简洁直观,对新手非常友好&#x…...

7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...

23-Oracle 23 ai 区块链表(Blockchain Table)
小伙伴有没有在金融强合规的领域中遇见,必须要保持数据不可变,管理员都无法修改和留痕的要求。比如医疗的电子病历中,影像检查检验结果不可篡改行的,药品追溯过程中数据只可插入无法删除的特性需求;登录日志、修改日志…...

centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...