シリーズ記事ディレクトリ
前回の記事Android Alphabet Index Sidebar (java版)に続き、Kotlin版の実装を完了し、Java版でいくつかの問題を解決しました(具体的な使い方については、現在のKotlinコード比較を参照してください)。
記事ディレクトリ
序文
通常の開発では、連絡先を開発する必要があるアプリケーションに遭遇した場合、Java で実装されたコードが Kotlin バージョンに変更され、使用中に以前のコードにいくつかの問題が見つかったため、このレターサイドバーは依然として非常に一般的です。たまたまkotlinコードも修正しました。
まだ未完成の問題があります:
1. 文字リストに加えて、他の値リストを設定し、サイドバー全体を再描画します
2. 選択した文字が効果を拡大します
時間があれば、これら 2 つの効果を引き続き実装することを検討できます。後でコードを追加します。
参照効果:動的効果については、以前の記事を参照してください。効果は同様です
提示:以下是本篇文章正文内容,下面案例可供参考
1. アイデア
1.通常のカスタムView、計測、描画の手順は必須です(今回はカスタムViewGroupではなく、onLayoutは使用しません)。
2. テキストの幅と高さを測定し (現在のテキスト リスト内の単一テキストの最大幅と高さを取得)、特定の幅と高さの測定値を計算し、setMeasuredDimension を使用して onMeasure の親ビューに設定します。 .
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 で使用します。これはコードを提供しません。誰もが知っている必要があります。
要約する
上記のコードは、レターの横に検索バーを実装するだけです. コードは参考用であり、必要に応じて変更できます.
記事の冒頭で未完成の質問を正しく完了し、後でコードを更新します。