Android-自定义View的实战学习总结

一、自定义View歌词界面

LrcView 类-->自定义的歌词视图

1. 构造函数和属性初始化

自定义 View 通常需要提供多个构造函数以支持不同的初始化方式。在 LrcView 中,提供了四个构造函数,最终调用 super 父类构造函数完成初始化, context.obtainStyledAttributes 方法获取自定义属性。

Kotlin 复制代码
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int
) : this(context, attrs, defStyleAttr, 0)
constructor(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
    val ta = context.obtainStyledAttributes(attrs, R.styleable.AbstractLrcView)
    // 获取自定义属性
    mNormalTextSize = ta.getDimension(
        R.styleable.AbstractLrcView_lrcTextSize,
        mResources.getDimension(R.dimen.lrc_text_size)
    )
    // ... 其他属性获取
    ta.recycle()
    // 初始化画笔等
    mNormalPaint.isAntiAlias = true
    mNormalPaint.textSize = mNormalTextSize
    // ... 其他画笔初始化
}
2. 测量和布局

onMeasure 方法 :虽然代码中未给出 onMeasure 方法,但在自定义 View 中,通常需要重写该方法来测量 View 的大小,以确定其宽度和高度。

onLayout 方法 :重写 onLayout 方法来确定子 View 的位置和大小,或者进行一些初始化操作。在 LrcView 中,当布局发生变化时,会重新设置播放按钮和时间线的边界,并初始化歌词列表。

Kotlin 复制代码
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    if (changed) {
        val width = mPlayDrawable.intrinsicWidth
        val height = mPlayDrawable.intrinsicHeight
        val l = (mTimeTextWidth - height) / 2
        val t = getHeight() / 2 - width / 2
        val r = l + width
        val b = t + width
        mPlayDrawable.setBounds(l, t, r, b)
        mTimeLineDrawable.setBounds(
            mTimeTextWidth,
            getHeight() / 2 - mTimeLineDrawable.intrinsicHeight / 2,
            getWidth() - mTimeTextWidth,
            getHeight() / 2 + mTimeLineDrawable.intrinsicHeight / 2
        )
        initEntryList()
    }
}
3.绘制

重写 onDraw 方法来绘制 View 的内容。在 LrcView 中,根据不同的歌词状态(显示歌词、加载歌词、无歌词等)绘制不同的内容,包括播放按钮、时间线、歌词文本等。

Kotlin 复制代码
@Synchronized
override fun onDraw(canvas: Canvas) {
    val centerY = height / 2
    when (mLrcStatus) {
        STATUS_SHOW_LRC -> {
            val centerLine = getCenterLine()
            if (mShowTimeline && !isSimpleMode) {
                // 绘制播放按钮
                mPlayDrawable.let {
                    it.setBounds(
                        mDrawableMargin,
                        centerY - mPlayDrawable.intrinsicHeight / 2,
                        mDrawableMargin + mPlayDrawable.intrinsicWidth,
                        centerY + mPlayDrawable.intrinsicHeight / 2
                    )
                    it.draw(canvas)
                }
                // 绘制时间线
                mTimePaint.color = mTimelineColor
                canvas.drawLine(
                    mTimeTextWidth.toFloat(),
                    centerY.toFloat(),
                    (getWidth() - mTimeTextWidth ).toFloat(),
                    centerY.toFloat(),
                    mTimePaint
                )
                mTimeLineDrawable.draw(canvas)
                // ... 其他绘制操作
            }
            // ... 绘制歌词文本
        }
        STATUS_LOADING_LRC -> {
            mNormalPaint.color = mCurrentTextColor
            drawText(canvas, mResources.getString(R.string.loading_lrc), centerY.toFloat())
        }
        STATUS_EMPTY_LRC -> {
            mNormalPaint.color = mCurrentTextColor
            drawText(canvas, mResources.getString(R.string.no_lrc), centerY.toFloat())
        }
        else -> {
            mNormalPaint.color = mCurrentTextColor
            drawText(canvas, mResources.getString(R.string.no_lrc), centerY.toFloat())
        }
    }
    super.onDraw(canvas)
}
4.事件处理

使用 GestureDetector 来处理触摸事件,如滑动、点击等。在 LrcView 中,通过 GestureDetector.SimpleOnGestureListener 监听不同的手势事件,并根据事件类型进行相应的处理。

Kotlin 复制代码
private val mSimpleOnGestureListener: GestureDetector.SimpleOnGestureListener =
    object : GestureDetector.SimpleOnGestureListener() {

        override fun onDown(event: MotionEvent): Boolean {
            if (hasLrc() && onPlayClickListener != null) {
                mScroller.forceFinished(true)
                mTouching = true
                mSlideing = false
                invalidate()
            }
            return true
        }

        override fun onScroll(
            e1: MotionEvent?,
            e2: MotionEvent,
            distanceX: Float,
            distanceY: Float
        ): Boolean {
            synchronized(this@LrcView) {
                if (hasLrc() && !isSimpleMode) {
                    // 添加播放按钮点击事件才能显示时间线
                    if (onPlayClickListener != null) {
                        removeCallbacks(hideTimelineRunnable)
                        mShowTimeline = true
                    }
                    if (!mSlideing) {
                        mSlideing = true
                        mSlideListener?.onSlideStart()
                    }
                    mOffset += -distanceY
                    mOffset = Math.min(mOffset, getOffset(0))
                    mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size - 1))
                    invalidate()
                    return true
                }
            }
            return super.onScroll(e1, e2, distanceX, distanceY)
        }

        // ... 其他手势事件处理
    }

// 在构造函数中初始化 GestureDetector
mGestureDetector = GestureDetector(context, mSimpleOnGestureListener)
mGestureDetector.setIsLongpressEnabled(false)

override fun onTouchEvent(event: MotionEvent): Boolean {
    return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event)
}
5.滚动处理

使用 Scroller 来实现平滑滚动效果。在 onFling 事件中,调用 mScroller.fling 方法启动滚动,并在 computeScroll 方法中更新滚动位置。虽然代码中未给出 computeScroll 方法,但通常的实现如下 :

Kotlin 复制代码
override fun computeScroll() {
    if (mScroller.computeScrollOffset()) {
        mOffset = mScroller.currY.toFloat()
        invalidate()
    }
}

SingleLineLrcView类---单行歌词类

测量方法 onMeasure
Kotlin 复制代码
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    var widthMeasureSpec = widthMeasureSpec
    var heightMeasureSpec = heightMeasureSpec
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    if (mMaxWidth > 0) {
        if (widthMode == MeasureSpec.EXACTLY) {
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.min(mMaxWidth.toInt(), MeasureSpec.getSize(widthMeasureSpec)), MeasureSpec.EXACTLY)
        } else {
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth.toInt(), MeasureSpec.EXACTLY)
        }
    }

    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    if (heightMode == MeasureSpec.EXACTLY) {
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(Math.max(mNormalTextSize, mCurrentTextSize).toInt(), MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.EXACTLY)
    } else {
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(mNormalTextSize, mCurrentTextSize).toInt(), MeasureSpec.EXACTLY)
    }
    setMeasuredDimension(getDefaultSize(suggestedMinimumWidth, widthMeasureSpec),
        getDefaultSize(suggestedMinimumHeight, heightMeasureSpec))
}
  • 根据 mMaxWidth 和测量模式调整宽度测量规格。
  • 根据文本大小和测量模式调整高度测量规格。
  • 最后调用 setMeasuredDimension 方法设置测量后的宽高
绘制方法 onDraw
Kotlin 复制代码
override fun onDraw(canvas: Canvas) {
    val centerY = (height / 2).toFloat()
    when (mLrcStatus) {
        STATUS_SHOW_LRC -> {
            if (mLastLrcTime >= 0) {
                updateShowLine()
                if (mLrcIndex >= 0 && mLrcIndex < mLrcEntryList.size) {
                    val text: String
                    if (mLrcIndex > 0 && mLrcEntryList[mLrcIndex - 1].time == mLrcEntryList[mLrcIndex].time) {
                        text = mLrcEntryList[mLrcIndex - 1].text
                    } else {
                        text = mLrcEntryList[mLrcIndex].text
                    }
                    if (!TextUtils.isEmpty(text)) {
                        drawFocusText(canvas, text, mCurrentPercent, centerY)
                    }
                }
            }
        }
        STATUS_LOADING_LRC -> {
            mNormalPaint.color = mCurrentTextColor
            drawText(canvas, mResources.getString(R.string.loading_lrc), centerY)
        }
        else -> {
            mNormalPaint.color = mCurrentTextColor
            drawText(canvas, mResources.getString(R.string.no_lrc), centerY)
        }
    }
    super.onDraw(canvas)
}
  • 根据 mLrcStatus 的不同状态,绘制不同的内容。
  • 如果状态为 STATUS_SHOW_LRC,则更新显示的歌词行,并调用 drawFocusText 方法绘制高亮的歌词。
  • 如果状态为 STATUS_LOADING_LRC,则绘制 "Loading lyrics" 的提示信息。
  • 其他状态则绘制 "没有歌词" 的提示信息。
自定义绘制方法
Kotlin 复制代码
private fun drawFocusText(canvas: Canvas, text: String, percent: Float, y: Float) {
    // ...
}

private fun drawText(canvas: Canvas, text: String, y: Float) {
    // ...
}
  • drawFocusText 方法用于绘制高亮的歌词,通过 canvas.clipRect 方法实现歌词的渐变效果。
  • drawText 方法用于绘制普通的文本。

歌词自定义View总结:

LrcView 类继承自 AbstractLrcView,实现了一个功能丰富的自定义歌词视图,综合运用了多个自定义 View 知识点。

在构造函数和属性初始化方面,提供了四个构造函数,最终调用父类构造函数完成初始化,并通过 context.obtainStyledAttributes 获取自定义属性,同时初始化画笔等。

测量和布局上,虽未给出 onMeasure 方法,但重写 onLayout 方法,在布局变化时重新设置播放按钮和时间线边界,并初始化歌词列表。

绘制时重写 onDraw 方法,依据不同歌词状态(显示、加载、无歌词等)绘制不同内容,如播放按钮、时间线、歌词文本等。事件处理使用 GestureDetector 监听不同手势事件并处理,像滑动、点击等。滚动处理借助 Scroller 实现平滑滚动,在 onFling 启动滚动,在 computeScroll 更新位置。还通过 R.styleable.AbstractLrcView 定义自定义属性,在构造函数中获取其值以配置外观和行为。最后,在 onDraw 方法中根据 mLrcStatus 不同状态绘制对应内容,实现状态管理。


SingleLineLrcView 是一个继承自 AbstractLrcView 的自定义 Android View,用于显示单行歌词。

它定义了正常和高亮文本的画笔、颜色、大小等属性,在构造函数中调用 init 方法进行初始化,通过 obtainStyledAttributes 获取自定义属性值并设置画笔属性和字体样式。提供了 setNormalColor 和 setCurrentColor 方法来动态改变文本颜色并刷新视图。

onMeasure 方法根据最大宽度和测量模式调整视图的宽高。onDraw 方法根据歌词状态(显示歌词、加载歌词、无歌词)绘制不同内容,若显示歌词则更新显示行并调用 drawFocusText 方法绘制高亮歌词,该方法通过 canvas.clipRect 实现歌词渐变效果;若加载歌词或无歌词则调用 drawText 方法绘制提示信息。此外,还提供了 getLrcWidth 方法用于计算歌词显示的宽度。

相关推荐
代码山河6 小时前
Python就业方向全解析:后端、爬虫、数据分析、AI你选哪个?
python·学习·架构·教程·面向对象·项目
voipmaker7 小时前
GB28181 对接踩坑实录:注册不上、目录不全、流卡顿,一次说清(附 2022 版变更清单)
物联网·音视频·实时音视频·信息与通信·iot·gb28181·国标gb28181
释厄6237 小时前
AI学习层更新与跨体转移律——人类社会同构·不缺理解缺 U
人工智能·学习
驰骋工作流7 小时前
流程时效考核的表结构:从待办时限到 WF_CH 考核结果驰骋BPM驰骋低代码低代码工作流引擎
android·低代码·rxjava
2601_958352908 小时前
23.5×19mm 换掉一整条音频链?|A-68 集成了什么:ADC / DAC / 24.576MHz 时钟 / AEC / 波束降噪,主控仅需 6 根线
前端·音视频·ai降噪·回声消除·语音模块
2601_965448878 小时前
AI视频制作:做系列历史人物视频音色不搭,AI 音色克隆的方法实测
人工智能·音视频
码上观世界8 小时前
多余的一声「嗯」:Fun-CosyVoice3 zero_shot 开头伪影排查实录
音视频
传奇开心果编程9 小时前
【Flutter踩坑排雷】第7课:巨型 build 函数坑|全部 UI 写在同一个 build,维护难、隐性异常
android·flutter·ios·移动开发·跨平台·dart·声明式ui
manyingAi9 小时前
AI漫剧生产系统架构解析:从文本拆解到音视频合成
人工智能·音视频
我是唐青枫9 小时前
Kotlin Delegation 委托详解
开发语言·kotlin