一、自定义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
方法用于计算歌词显示的宽度。