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 获取自定义属性值并设置画笔属性和字体样式。提供了 setNormalColorsetCurrentColor 方法来动态改变文本颜色并刷新视图。

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

相关推荐
DKPT4 小时前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
哲科软件6 小时前
跨平台开发的抉择:Flutter vs 原生安卓(Kotlin)的优劣对比与选型建议
android·flutter·kotlin
好好研究6 小时前
学习栈和队列的插入和删除操作
数据结构·学习
新中地GIS开发老师7 小时前
新发布:26考研院校和专业大纲
学习·考研·arcgis·大学生·遥感·gis开发·地理信息科学
SH11HF8 小时前
小菜狗的云计算之旅,学习了解rsync+sersync实现数据实时同步(详细操作步骤)
学习·云计算
Frank学习路上8 小时前
【IOS】XCode创建firstapp并运行(成为IOS开发者)
开发语言·学习·ios·cocoa·xcode
Chef_Chen9 小时前
从0开始学习计算机视觉--Day07--神经网络
神经网络·学习·计算机视觉
站在巨人肩膀上的码农11 小时前
全志T507 音频ALSA核心层注册流程分析
驱动开发·音视频·安卓·全志·alsa·声卡
X_StarX11 小时前
【Unity笔记02】订阅事件-自动开门
笔记·学习·unity·游戏引擎·游戏开发·大学生
MingYue_SSS11 小时前
开关电源抄板学习
经验分享·笔记·嵌入式硬件·学习