Android自定義View--仿QQ音樂歌詞
0.前言
國慶長假,祝大家節日愉快,這個控制元件其實是上週五寫的,以前寫程式碼一直都是信馬由韁,無拘無束,但是最近開始注重時間和效率,喜歡限時程式設計,今天這個控制元件用了4個小時。。。遠超當初預訂的2個半小時,主要是中間弄了個防火演習,閒話不說,先看效果。

image
1.分析
列一下功能點:
1.解析lrc格式的檔案生成List<LyricsItem>
2.繪製歌詞,繪製高亮歌詞
3.高亮歌詞移動到中間位置,換行時滾動到中間位置
4.新增滑動事件,快速滑動事件。
2.程式碼
2.1解析lrc格式的檔案生成List<LyricsItem>
關於lrc歌詞文字,以下摘自百度百科:
lrc歌詞文字中含有兩類標籤:
一是標識標籤,其格式為“[標識名:值]”主要包含以下預定義的標籤:
[ar:歌手名]、[ti:歌曲名]、[al:專輯名]、[by:編輯者(指lrc歌詞的製作人)]、[offset:時間補償值] (其單位是毫秒,正值表示整體提前,負值相反。這是用於總體調整顯示快慢的,但多數的MP3可能不會支援這種標籤)。
二是時間標籤,形式為“[mm:ss]”或“[mm:ss.ff]”(分鐘數:秒數.百分之一秒數 [2] ),時間標籤需位於某行歌詞中的句首部分,一行歌詞可以包含多個時間標籤(比如歌詞中的迭句部分)。當歌曲播放到達某一時間點時,MP3就會尋找對應的時間標籤並顯示標籤後面的歌詞文字,這樣就完成了“歌詞同步”的功能。
這裡我們使用的是抖音上那首很火的that girl那首歌
[ti:That Girl] [ar:Morris����] [al:That Girl] [by:] [offset:0] [00:00.00]That Girl - Morris���� [00:00.13]Lyricist��Stephen Paul Robson/Olly Murs/Claude Kelly [00:00.34]Composer/">Composer��Stephen Paul Robson/Olly Murs/Claude Kelly [00:00.56]There's a girl but I let her get away [00:05.57]It's all my fault cause pride got in the way [00:11.12]And I'd be lying if I said I was okay [00:16.60]About that girl the one I let get away [00:21.40]I keep saying no [00:23.82]This can't be the way it was supposed to be [00:26.92]I keep saying no [00:29.43]There's gotta be a way to get you close to me [00:32.54]Now I know you gotta speak up if you want somebody [00:36.63]Can't let them get away oh no [00:39.26]You don't wanna end up sorry [00:41.90]The way that I'm feeling everyday [00:43.77]Don't you know [00:44.75]No no no no [00:47.26]There's no home for the broken heart [00:49.52]Don't you know [00:50.06]No no no no [00:52.68]There's no home for the broken [00:54.44]There's a girl but I let her get away [00:59.69]It's my fault cause I said I needed space [01:05.14]And I've been torturing myself night and day [01:10.43]About that girl the one I let get away [01:15.42]I keep saying no [01:17.96]This can't be the way it was supposed to be [01:20.80]I keep saying no [01:23.32]There's gotta be a way [01:24.54]There's gotta be a way [01:25.72]To get you close to me [01:27.13]You gotta speak up if you want somebody [01:30.50]Can't let them get away oh no [01:33.09]You don't wanna end up sorry [01:35.80]The way that I'm feeling everyday [01:37.91]Don't you know [01:38.66]No no no no [01:41.18]There's no home for the broken heart [01:43.22]Don't you know [01:44.12]No no no no [01:46.64]There's no home for the broken [01:49.42]No home for me [01:52.10]No home cause I'm broken [01:54.76]No room to breathe [01:56.83]And I got no one to blame [02:00.11]No home for me [02:02.88]No home cause I'm broken [02:04.67]About that girl [02:06.41]The one I let get away [02:09.57]So you better [02:10.44]Speak up [02:13.54]You can't let them get away oh no [02:16.36]You don't wanna end up sorry [02:18.90]The way that I'm feeling everyday [02:21.02]Don't you know [02:21.97]No no no no [02:24.39]There's no home for the broken heart [02:26.23]Don't you know [02:27.26]No no no no [02:29.73]There's no home for the broken [02:31.66]Oh [02:32.82]You don't wanna lose that love [02:34.90]It's only gonna hurt too much [02:36.85]I'm telling you [02:38.18]You don't wanna lose that love [02:40.30]It's only gonna hurt too much [02:42.28]I'm telling you [02:43.50]You don't wanna lose that love [02:45.32]Cause there's no hope for the broken heart [02:47.68]About that girl [02:49.45]The one I let get away
以下是對lrc歌詞的解析,解析後程程一個List<LyricsItem>,每個LyricsItem代表一行歌詞
data class LyricsItem(var ti:String="",var ar:String="",var al:String="",var by:String="",var offset:Long=0,var start:Long=0,var duration:Long=0,var lyrics:String="")
private fun readLrc(): List<LyricsItem> { val result = mutableListOf<LyricsItem>() try { val lyricInput = BufferedReader(InputStreamReader(assets.open("thatgirl.lrc"))) var line = lyricInput.readLine() while (line != null) { val lyricItem = parse(line) if(result.size==6) { for(i in 0 until 5) { result[i].start=i*lyricItem.start/5 result[i].duration=lyricItem.start/5 } } else if (result.size > 6) { result[result.size - 1].duration = lyricItem.start - result[result.size - 1].start } result.add(lyricItem) line=lyricInput.readLine() } lyricInput.close() } catch (e: Exception) { e.printStackTrace() } return result } private fun parse(line: String): LyricsItem { val lyricsItem = LyricsItem() val pattern = Pattern.compile("^(\\[(.*?)\\])(.*?)$") val matcher = pattern.matcher(line) if (matcher.find()) { val front = matcher.group(2) when { front.contains("ti") -> lyricsItem.ti = front.split(":")[1] front.contains("ar") -> lyricsItem.ar = front.split(":")[1] front.contains("al") -> lyricsItem.al = front.split(":")[1] front.contains("by") -> lyricsItem.by = front.split(":")[1] front.contains("offset")->lyricsItem.offset=front.split(":")[1].toLong() else -> { val timeArray = front.split(":") val secondTimeArray=timeArray[1].split(".") val second=secondTimeArray[0].toLong() val micSecond=secondTimeArray[1].toLong() lyricsItem.start = (timeArray[0].toLong() * 60 + second)*1000+micSecond lyricsItem.lyrics = matcher.group(3) } } } return lyricsItem }
2.2繪製歌詞,繪製高亮歌詞
接著看LyricsView的onDraw方法,這裡對歌詞進行了繪製
override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.let { val dstBitmap = Bitmap.createBitmap(width, lyricsHeight, Bitmap.Config.ARGB_8888) val dstCanvas = Canvas(dstBitmap) drawInfo(dstCanvas) drawLyrics(dstCanvas) drawHighlight(dstBitmap, it) } }
其中drawInfo方法用來繪製這首歌的一些資訊,例如歌手,名稱,專輯,作詞作曲等
private fun drawInfo(canvas: Canvas) { for (i in 0 until 4) { drawLyricItem(canvas, lyricsList[i], i) } }
drawLyrics方法用來繪製歌詞
private fun drawLyrics(canvas: Canvas) { for (i in 5 until lyricsList.size) { drawLyricItem(canvas, lyricsList[i], i) } } private fun drawLyricItem(canvas: Canvas, lyricsItem: LyricsItem, index: Int) { paint.color = normalTextColor val centerX = width.toFloat() / 2 val textBound = Rect() val lyricContent = getLyricDrawContent(lyricsItem) paint.getTextBounds(lyricContent, 0, lyricContent.length, textBound) val topOffset = lineHeight.toFloat() * index canvas.drawText(lyricContent, centerX - textBound.width() / 2, topOffset + lineHeight / 2 + textBound.height() / 2, paint) }
drawHightlight方法用來繪製高亮的歌詞,也就是唱到的那個歌詞
private fun drawHighlight(dstBitmap: Bitmap, canvas: Canvas) { val centerX = width.toFloat() / 2 paint.color = normalTextColor paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) canvas.drawBitmap(dstBitmap, 0f, 0f, paint) val lyrics = lyricsList[highLightPos] val lyricsContent = getLyricDrawContent(lyrics) val textBound = Rect() val topOffset = highLightPos * lineHeight.toFloat() paint.getTextBounds(lyricsContent, 0, lyricsContent.length, textBound) val offset = (time.toFloat() - lyrics.start) / lyrics.duration.toFloat() paint.color = highlightTextColor canvas.drawRect(centerX - textBound.width() / 2, topOffset, centerX - textBound.width() / 2 + offset * textBound.width(), topOffset + lineHeight, paint) paint.xfermode = null dstBitmap.recycle() }
2.3高亮歌詞移動到中間位置,換行時滾動到中間位置
歌詞的換行移動這裡是通過改變scrolley來實現的,首選需要不停的upadet這個控制元件,將播放時間傳入,然後計算出需要進行高亮的歌詞進行繪製,並且如果沒有進行觸控的話,就將高亮的那行歌詞移動到中間位置
fun update(time: Long) { this.time = time for (i in 0 until lyricsList.size) { val lyricsItem = lyricsList[i] if (isInRange(time, lyricsItem)) { if (highLightPos != i) { highLightPos = i if (!isTouching && !isScrolling) { scrollToPosition(i) } } else { postInvalidate() } } } }
2.4.新增滑動事件,快速滑動事件。
複寫onTouchEvent時間,根據滑動距離來跟新scrolly,如果是快速滑動的話則計算出速度,然後讓其滑動0.5秒。
override fun onTouchEvent(event: MotionEvent?): Boolean { val velocityTracker = VelocityTracker.obtain() velocityTracker.addMovement(event) when (event?.action) { MotionEvent.ACTION_DOWN -> { removeCallbacks(resetCallback) touchY = event.y isTouching = true } MotionEvent.ACTION_MOVE -> { scrollY -= (event.y - touchY).toInt() scrollY = Math.min(MAX_SCROLLY, Math.max(scrollY, MIN_SCROLLY)) touchY = event.y velocityTracker.computeCurrentVelocity(1000) speed = velocityTracker.yVelocity.toInt() } MotionEvent.ACTION_UP -> { velocityTracker.clear() velocityTracker.recycle() scrollOffset(-speed,500) postDelayed(resetCallback, 2000) } } return true }
3.專案地址
ofollow,noindex">github

image
關注我的公眾號