Android自定義View–仿QQ音樂歌詞

滑板上的老砒霜發表於2019-03-02

0.前言

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

Android自定義View–仿QQ音樂歌詞

1.分析

列一下功能點:
1.解析lrc格式的檔案生成List
2.繪製歌詞,繪製高亮歌詞
3.高亮歌詞移動到中間位置,換行時滾動到中間位置
4.新增滑動事件,快速滑動事件。

2.程式碼

2.1解析lrc格式的檔案生成List

關於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��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代表一行歌詞

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.專案地址

github

Android自定義View–仿QQ音樂歌詞

關注我的公眾號

相關文章