一、自定義View歌詞界面
LrcView
?類-->自定義的歌詞視圖
1. 構造函數和屬性初始化
????????自定義 View 通常需要提供多個構造函數以支持不同的初始化方式。在?LrcView
?中,提供了四個構造函數,最終調用?super
?父類構造函數完成初始化,?context.obtainStyledAttributes
?方法獲取自定義屬性。
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 = truemNormalPaint.textSize = mNormalTextSize// ... 其他畫筆初始化
}
2. 測量和布局
onMeasure
?方法:雖然代碼中未給出?onMeasure
?方法,但在自定義 View 中,通常需要重寫該方法來測量 View 的大小,以確定其寬度和高度。
onLayout
?方法:重寫?onLayout
?方法來確定子 View 的位置和大小,或者進行一些初始化操作。在?LrcView
?中,當布局發生變化時,會重新設置播放按鈕和時間線的邊界,并初始化歌詞列表。
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.intrinsicWidthval height = mPlayDrawable.intrinsicHeightval l = (mTimeTextWidth - height) / 2val t = getHeight() / 2 - width / 2val r = l + widthval b = t + widthmPlayDrawable.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
?中,根據不同的歌詞狀態(顯示歌詞、加載歌詞、無歌詞等)繪制不同的內容,包括播放按鈕、時間線、歌詞文本等。
@Synchronized
override fun onDraw(canvas: Canvas) {val centerY = height / 2when (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 = mTimelineColorcanvas.drawLine(mTimeTextWidth.toFloat(),centerY.toFloat(),(getWidth() - mTimeTextWidth ).toFloat(),centerY.toFloat(),mTimePaint)mTimeLineDrawable.draw(canvas)// ... 其他繪制操作}// ... 繪制歌詞文本}STATUS_LOADING_LRC -> {mNormalPaint.color = mCurrentTextColordrawText(canvas, mResources.getString(R.string.loading_lrc), centerY.toFloat())}STATUS_EMPTY_LRC -> {mNormalPaint.color = mCurrentTextColordrawText(canvas, mResources.getString(R.string.no_lrc), centerY.toFloat())}else -> {mNormalPaint.color = mCurrentTextColordrawText(canvas, mResources.getString(R.string.no_lrc), centerY.toFloat())}}super.onDraw(canvas)
}
4.事件處理
使用?GestureDetector
?來處理觸摸事件,如滑動、點擊等。在?LrcView
?中,通過?GestureDetector.SimpleOnGestureListener
?監聽不同的手勢事件,并根據事件類型進行相應的處理。
private val mSimpleOnGestureListener: GestureDetector.SimpleOnGestureListener =object : GestureDetector.SimpleOnGestureListener() {override fun onDown(event: MotionEvent): Boolean {if (hasLrc() && onPlayClickListener != null) {mScroller.forceFinished(true)mTouching = truemSlideing = falseinvalidate()}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 = truemSlideListener?.onSlideStart()}mOffset += -distanceYmOffset = 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
?方法,但通常的實現如下????????:
override fun computeScroll() {if (mScroller.computeScrollOffset()) {mOffset = mScroller.currY.toFloat()invalidate()}
}
SingleLineLrcView類---單行歌詞類
?測量方法?onMeasure
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {var widthMeasureSpec = widthMeasureSpecvar heightMeasureSpec = heightMeasureSpecval 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
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: Stringif (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 = mCurrentTextColordrawText(canvas, mResources.getString(R.string.loading_lrc), centerY)}else -> {mNormalPaint.color = mCurrentTextColordrawText(canvas, mResources.getString(R.string.no_lrc), centerY)}}super.onDraw(canvas)
}
- 根據?
mLrcStatus
?的不同狀態,繪制不同的內容。 - 如果狀態為?
STATUS_SHOW_LRC
,則更新顯示的歌詞行,并調用?drawFocusText
?方法繪制高亮的歌詞。 - 如果狀態為?
STATUS_LOADING_LRC
,則繪制 “Loading lyrics” 的提示信息。 - 其他狀態則繪制 “沒有歌詞” 的提示信息。
?自定義繪制方法
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
?方法用于計算歌詞顯示的寬度。?