系列文章目录
接上一篇Android字母索引侧边栏(java版本),完成Kotlin版本的实现,并且解决java版本中的一些问题(具体使用可以参考对比当前这篇Kotlin的代码)。
前言
平常开发中遇到需要开发联系人的应用,这个字母侧边栏还是挺常用的,因为以前是java实现的代码,现在刚好改成Kotlin的版本,在使用中也发现一些以前的代码的问题,刚好也在kotlin代码中做一个修正。
还有未完成的问题:
1、除开字母列表之外,设置其他值列表,并且重新绘制整个侧边栏
2、选中的字母放大效果
这两个效果等有时间我可以考虑继续实现,后续补充代码
参考效果:动态的效果可以参考上一篇文章,效果类似
提示:以下是本篇文章正文内容,下面案例可供参考
一、思路
1、正常的自定义View的几个步骤,测量、绘制不能少(本次不是自定义ViewGroup,也没有用到onLayout)。
2、测量文本的宽高(都取当前文本列表单个文本最大的宽高),然后计算出具体的宽、高测量值,并且在onMeasure中使用setMeasuredDimension设置给父View。
3、最终就是在onDraw中绘制
二、实现代码
1. 各资源值
colors.xml和dimens.xml值代码如下(示例):
<!--LetterSidebar-->
<color name="side_text_normal_color">#000000</color>
<color name="side_text_select_color">#000000</color>
<color name="side_select_shape_color">#3ACF40</color>
<!--LetterSidebar-->
<dimen name="side_text_normal_size">12sp</dimen>
<dimen name="side_text_select_size">12sp</dimen>
<!--文本绘制的过程中,默认增加的偏移量,为了选中背景的绘制-->
<dimen name="side_default_offset_wh">2dp</dimen>
自定义属性
<!--字母搜索侧边栏-->
<declare-styleable name="LetterSidebar">
<attr name="side_text_normal_size" format="dimension"/>
<attr name="side_text_select_size" format="dimension"/>
<attr name="side_text_normal_color" format="color"/>
<attr name="side_text_select_color" format="color"/>
<attr name="side_text_gravity" format="enum">
<enum name="start" value="1"/>
<enum name="center" value="2"/>
</attr>
<attr name="side_select_shape" format="enum">
<enum name="circle" value="1"/>
<enum name="square" value="2"/>
</attr>
<attr name="side_select_shape_color" format="color"/>
</declare-styleable>
2.代码实现
代码如下(示例):
private const val TAG = "LetterSidebar"
private const val TWO_TIMES = 2
/**
* 字母侧边栏.
*/
class LetterSidebar: View {
/**
* 字母选中背景形状,圆、矩形。
*/
annotation class SelectShape {
companion object {
// 圆
const val CIRCLE = 1
// 矩形
const val SQUARE = 2
}
}
/**
* 绘制的字母位置,从左开始或者居中.
*/
annotation class TextGravityY {
companion object {
// 从左开始
const val GRAVITY_START = 1
// 居中
const val GRAVITY_CENTER = 2
}
}
// 字母和特殊符号列表
private val mLetterList: MutableList<String> = mutableListOf()
// 未选中文字大小
private var mTextNormalSize = 0f
// 选中文字大小
private var mTextSelectSize = 0f
// 文本绘制的过程中,默认增加的偏移量(乘以2使用,因为同时给上下左右增加),为了选中背景的绘制
private var mDefaultOffsetWh = 0f
// 未选中文字颜色
private var mTextNormalColor = 0
// 选中文字颜色
private var mTextSelectColor = 0
// 文字显示位置
private var mTextGravity = TextGravityY.GRAVITY_CENTER
// 选中之后的背景图形
private var mSelectShape = SelectShape.CIRCLE
// 选中之后背景图形颜色
private var mSelectShapeColor = 0
// 选中背景图形半径
private var mSelectShapeRadius = 0f
// 控件的默认宽高
private var mDefaultWidth = 0
private var mDefaultHeight = 0
// 文字的画笔
private var mTextPaint: Paint? = null
// 选中背景的画笔
private var mShapePaint: Paint? = null
// 控件的宽高
private var mWidth = 0
private var mHeight = 0
// 触摸选中的位置, 默认未触摸选中
private var mPosition = -1
// 记录上一次触摸的位置,避免重复调用
private var mPrePosition = -1
// 计算单个字符所占用的高度
private var mSingleTxtHeight = 0f
// 判断当前手指是否触摸在View上
private var mIsTouch = false
// 回调监听
private var mOnLetterChangedListener: OnLetterChangedListener? = null
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
initAttrs(attrs)
}
private fun init() {
// 给字母列表添加字母和特殊符号
for (i in 'A'.code..'Z'.code + 1) {
val ch = if (i > 'Z'.code) {
'#'
} else {
i.toChar()
}
mLetterList.add(ch.toString())
}
mTextNormalSize = context.resources.getDimension(R.dimen.side_text_normal_size)
mTextSelectSize = context.resources.getDimension(R.dimen.side_text_select_size)
mDefaultOffsetWh = context.resources.getDimension(R.dimen.side_default_offset_wh)
mTextNormalColor = context.getColor(R.color.side_text_normal_color)
mTextSelectColor = context.getColor(R.color.side_text_select_color)
mSelectShapeColor = context.getColor(R.color.side_select_shape_color)
mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mShapePaint = Paint(Paint.ANTI_ALIAS_FLAG)
}
private fun initAttrs(attrs: AttributeSet?) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LetterSidebar)
mTextNormalSize = typedArray.getDimension(R.styleable.LetterSidebar_side_text_normal_size, mTextNormalSize)
mTextSelectSize = typedArray.getDimension(R.styleable.LetterSidebar_side_text_select_size, mTextSelectSize)
mTextNormalColor = typedArray.getColor(R.styleable.LetterSidebar_side_text_normal_color, mTextNormalColor)
mTextSelectColor = typedArray.getColor(R.styleable.LetterSidebar_side_text_select_color, mTextSelectColor)
mTextGravity = typedArray.getInt(R.styleable.LetterSidebar_side_text_gravity, mTextGravity)
mSelectShape = typedArray.getInt(R.styleable.LetterSidebar_side_select_shape, mSelectShape)
mSelectShapeColor = typedArray.getColor(R.styleable.LetterSidebar_side_select_shape_color, mSelectShapeColor)
typedArray.recycle()
mTextPaint?.color = mTextNormalColor
mTextPaint?.textSize = mTextNormalSize
mShapePaint?.color = mSelectShapeColor
mDefaultWidth = if (mTextNormalSize > mTextSelectSize) {
(mTextNormalSize + mDefaultOffsetWh.times(TWO_TIMES)).toInt()
} else {
(mTextSelectSize + mDefaultOffsetWh.times(TWO_TIMES)).toInt()
}
mDefaultHeight = getDefaultHeight()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = getViewSize(mDefaultWidth + paddingStart + paddingEnd, widthMeasureSpec)
val height = getViewSize(mDefaultHeight + paddingTop + paddingBottom, heightMeasureSpec)
// Logger.d(TAG, "getViewSize mDefaultHeight:: $mDefaultHeight, height:: $height")
// Logger.d(TAG, "getViewSize paddingStart::$paddingStart, paddingEnd::$paddingEnd, paddingTop::$paddingTop, paddingBottom::$paddingBottom")
setMeasuredDimension(width, height)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mWidth = w
mHeight = h
if (mLetterList.isEmpty()) {
return
}
mSingleTxtHeight = (mHeight - paddingTop - paddingBottom).toFloat().div(mLetterList.size)
// 选中背景圆形的半径
mSelectShapeRadius = mSingleTxtHeight.div(TWO_TIMES)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
for (i in 0 until mLetterList.size) {
if (i == mPosition) {
drawSelect(canvas, mLetterList[i], i)
} else {
drawNormal(canvas, mLetterList[i], i)
}
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
mIsTouch = false
when (event?.action) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE -> {
mIsTouch = true
// 获取触摸位置的Y坐标
val y = event.y
mPosition = getPosition(y)
if (mPosition != mPrePosition && mPosition >= 0) {
mOnLetterChangedListener?.onChanged(mLetterList[mPosition], mPosition)
mPrePosition = mPosition
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
mPosition = -1
mIsTouch = false
}
else -> {
mIsTouch = false
}
}
invalidate()
mOnLetterChangedListener?.onTouch(mIsTouch)
return mIsTouch
}
private fun getViewSize(size: Int, measureSpec: Int): Int {
var result = size
val specMode = MeasureSpec.getMode(measureSpec)
val specSize = MeasureSpec.getSize(measureSpec)
when (specMode) {
MeasureSpec.EXACTLY -> {
result = specSize
}
MeasureSpec.AT_MOST -> {
result = min(size, specSize)
}
MeasureSpec.UNSPECIFIED -> {
result = size
}
}
return result
}
private fun getDefaultHeight(): Int {
if (mLetterList.isEmpty()) {
return 0
}
mTextPaint?.let {
paint ->
paint.textSize = if (mTextNormalSize > mTextSelectSize) {
mTextNormalSize
} else {
mTextSelectSize
}
//var tempHeight = 0
var maxLetterHeight = 0
for (letter in mLetterList) {
//tempHeight += (getTextHeight(letter, paint) + mDefaultOffsetWh.times(TWO_TIMES).toInt())
val textHeight = getTextHeight(letter, paint)
if (textHeight > maxLetterHeight) {
maxLetterHeight = textHeight
}
}
return (maxLetterHeight + mDefaultOffsetWh.times(TWO_TIMES)).times(mLetterList.size).toInt()
}
return 0
}
/**
* 获取文字的高度.
*
* @return 文本高度
*/
private fun getTextHeight(text: String, paint: Paint): Int {
val rect = Rect()
paint.getTextBounds(text, 0, text.length, rect)
return rect.bottom - rect.top
}
/**
* 计算当前触摸的字母的position。
*
* @param y 当前触摸的屏幕的位置
* @return 返回字母的position
*/
private fun getPosition(y: Float): Int {
return if (y < paddingTop || y > mHeight - paddingBottom || mHeight <= 0 || mLetterList.isEmpty()) {
-1
} else {
(y - paddingTop).div(mHeight - paddingTop - paddingBottom).times(mLetterList.size).toInt()
}
}
/**
* 绘制选中的样式.
*
* @param canvas 画布
* @param letter 需要绘制的字母
* @param index 绘制的下标
*/
private fun drawSelect(canvas: Canvas?, letter: String, index: Int) {
if (canvas == null) {
return
}
mTextPaint?.let {
paint ->
paint.color = mTextSelectColor
paint.textSize = mTextSelectSize
val letterWidth = paint.measureText(letter)
val letterHeight = getTextHeight(letter, paint)
// 计算文本绘制的(x,y),默认是该字母的居中绘制坐标
val xPos = if (mTextGravity == TextGravityY.GRAVITY_CENTER) {
// 绘制在中间
paddingStart + (mWidth - paddingStart - paddingEnd - letterWidth).div(TWO_TIMES)
} else {
// 从左侧开始绘制
paddingStart.toFloat()
}
var textOffset = 0f
if (mSingleTxtHeight > letterHeight) {
textOffset = (mSingleTxtHeight - letterHeight).div(TWO_TIMES)
}
val yPos = paddingTop + mSingleTxtHeight.times(index + 1) - textOffset
// 绘制背景
if (mSelectShape == SelectShape.CIRCLE) {
val cy = paddingTop + mSingleTxtHeight.times(index + 1) - mSelectShapeRadius
val cx = paddingStart + (mWidth - paddingStart - paddingEnd).div(TWO_TIMES)
mShapePaint?.let {
canvas.drawCircle(cx.toFloat(), cy, mSelectShapeRadius, it)
}
} else {
val left = paddingStart.toFloat()
val top = paddingTop + mSingleTxtHeight.times(index)
val right = (mWidth - paddingEnd).toFloat()
val bottom = paddingTop + mSingleTxtHeight.times(index + 1)
mShapePaint?.let {
canvas.drawRect(left, top, right, bottom, it)
}
}
// 绘制文本
canvas.drawText(letter, xPos, yPos, paint)
}
}
/**
* 绘制默认的样式.
*
* @param canvas 画布
* @param letter 需要绘制的字母
* @param index 绘制的下标
*/
private fun drawNormal(canvas: Canvas?, letter: String, index: Int) {
if (canvas == null) {
return
}
mTextPaint?.let {
paint ->
paint.color = mTextNormalColor
paint.textSize = mTextNormalSize
val letterWidth = paint.measureText(letter)
val letterHeight = getTextHeight(letter, paint)
// 计算文本绘制的(x,y),默认是该字母的居中绘制坐标
val xPos: Float = if (mTextGravity == TextGravityY.GRAVITY_CENTER) {
// 绘制在中间
paddingStart + (mWidth - paddingStart - paddingEnd - letterWidth).div(TWO_TIMES)
} else {
// 从左侧开始绘制
paddingStart.toFloat()
}
var textOffset = 0f
if (mSingleTxtHeight > letterHeight) {
textOffset = (mSingleTxtHeight - letterHeight).div(TWO_TIMES)
}
val yPos = paddingTop + mSingleTxtHeight.times(index + 1) - textOffset
canvas.drawText(letter, xPos, yPos, paint)
}
}
/**
* 设置字母侧边栏回调监听.
*
* @param listener 回调监听
*/
fun setOnLetterChangedListener(listener: OnLetterChangedListener) {
this.mOnLetterChangedListener = listener
}
/**
* 设置字母侧边栏回调监听.
*
* @param onChanged 选中字母的监听.
* @param onTouch 是否被触摸.
*/
fun setOnLetterChangedListener(
onChanged: (letter: String, position: Int) -> Unit,
onTouch: (isTouch: Boolean) -> Unit
) {
mOnLetterChangedListener = object : OnLetterChangedListener {
override fun onChanged(letter: String, position: Int) {
onChanged(letter, position)
}
override fun onTouch(isTouch: Boolean) {
onTouch(isTouch)
}
}
}
interface OnLetterChangedListener {
/**
* 选中字母的监听.
*
* @param letter 选中的字母
* @param position 选中字母的下标
*/
fun onChanged(letter: String, position: Int)
/**
* 是否被触摸.
*
* @param isTouch {@true} 触摸
*/
fun onTouch(isTouch: Boolean)
}
}
最后就是具体使用,在xml中使用,这就不给代码了,大家应该都会
总结
以上代码简单实现了字母侧边搜索栏,代码仅供参考,大家可以根据自己需求修改。
正确后续完成文章开头未完成的问题,后续更新代码。