安卓 车轮视图 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…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...

SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...

【7色560页】职场可视化逻辑图高级数据分析PPT模版
7种色调职场工作汇报PPT,橙蓝、黑红、红蓝、蓝橙灰、浅蓝、浅绿、深蓝七种色调模版 【7色560页】职场可视化逻辑图高级数据分析PPT模版:职场可视化逻辑图分析PPT模版https://pan.quark.cn/s/78aeabbd92d1...

短视频矩阵系统文案创作功能开发实践,定制化开发
在短视频行业迅猛发展的当下,企业和个人创作者为了扩大影响力、提升传播效果,纷纷采用短视频矩阵运营策略,同时管理多个平台、多个账号的内容发布。然而,频繁的文案创作需求让运营者疲于应对,如何高效产出高质量文案成…...

基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...

排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...

免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...