android實(shí)現(xiàn)音樂(lè)跳動(dòng)效果

效果圖

dancer.gif

實(shí)現(xiàn)

整體的流程圖如下

在這里插入圖片描述

上面主要步驟分為3個(gè)
1眉枕、計(jì)算寬度能放下多少列的音頻塊歌径。
2亿絮、計(jì)算每一列中音頻塊的個(gè)數(shù)
3鼠锈、繪制音頻塊

1、計(jì)算寬度能放下多少列的音頻塊晨抡。
設(shè)置音頻塊的寬度為danceWidth氛悬,音頻塊橫向之間的間距為danceGap则剃,那么可以算出能放的列數(shù):

/**
         * 先計(jì)算當(dāng)前寬度能夠放下多少個(gè)音頻塊
         */
        val widthNum = (getAvailableWith() / (danceGap + danceWidth)).toInt()

  /**
     * 獲取可以用的寬度
     */
    private fun getAvailableWith() = mCanvasWidth - paddingLeft - paddingRight

2、計(jì)算每一列中音頻塊的個(gè)數(shù)
在算出橫向能放置多少音頻塊后如捅,遍歷橫棍现,然后繪制列中的音頻塊,列中的音頻塊的個(gè)數(shù)跟音頻的高低相關(guān)镜遣,這里實(shí)現(xiàn)方式是通過(guò)Visualizer這個(gè)類(lèi)然后獲取到mRawAudioBytes數(shù)組己肮,

 mVisualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
            @Override
            public void onWaveFormDataCapture(Visualizer visualizer, byte[] bytes,
                                              int samplingRate) {
                BaseVisualizer.this.mRawAudioBytes = bytes;
                invalidate();
            }

            @Override
            public void onFftDataCapture(Visualizer visualizer, byte[] bytes,
                                         int samplingRate) {
            }
        }, Visualizer.getMaxCaptureRate() / 2, true, false);

這里設(shè)置的獲取的mRawAudioBytes數(shù)組的大小是128,數(shù)組的區(qū)間范圍[-128,127],計(jì)算列的時(shí)候這里做了兩個(gè)比較重要的操作悲关,第一個(gè)是怎么把mRawAudioBytes數(shù)組的值與音頻的個(gè)數(shù)做映射朴肺,第二個(gè)是怎么取mRawAudioBytes數(shù)組的值。

 /**
         * 先計(jì)算當(dāng)前寬度能夠放下多少個(gè)音頻塊
         */
        val widthNum = (getAvailableWith() / (danceGap + danceWidth)).toInt()
        Log.d(
            TAG,
            "widthNum $widthNum"
        )
        /**
         * 算出橫向能放多少后坚洽,進(jìn)行繪制
         */

        /**
         * 繪制的時(shí)候用于標(biāo)記開(kāi)始繪制的位置
         */
        var lastDanceRight = paddingLeft.toFloat()
        if (widthNum > 0 && mRawAudioBytes != null && mRawAudioBytes.isNotEmpty())
            for (i in 0 until widthNum) {
                //先算出當(dāng)前高度,然后再算這個(gè)高度能放下多少個(gè)音頻塊
                val num = (getAvailableHeight() / (danceHeight + danceGap)).toInt()
                val index = (mRawAudioBytes.size) * (i.toFloat() / widthNum)
                val b = (mRawAudioBytes[index.toInt()] + 128).toFloat() / 255f
                var heightNum =
                    (b * num).toInt()
                if (heightNum < miniNum) {
                    heightNum = miniNum
                }
                if (heightNum > maxNum) {
                    heightNum = maxNum
                }
                //拿到最頂部的高度
                var lastHeight = mCanvasHeight - paddingStart.toFloat()
                Log.d(
                    TAG,
                    "heightNum $heightNum lastHeight $lastHeight lastDanceRight $lastDanceRight ${mRawAudioBytes[i]} $num $b $index"
                )
                lastHeight = drawItem(heightNum, lastDanceRight, lastHeight, canvas)
                lastDanceRight += danceWidth + danceGap
            }

上面做了兩個(gè)映射西土,首先可能有0~n橫讶舰,但是mRawAudioBytes大小是128,遍歷橫的時(shí)候?qū)ο聵?biāo)進(jìn)行一個(gè)映射需了,保證獲得的值是均勻的跳昼,

/**
通過(guò)這個(gè)映射得到index
*/
val index = (mRawAudioBytes.size) * (i.toFloat() / widthNum)

第二個(gè)映射,是得到了代表音頻大小的mRawAudioBytes數(shù)組肋乍,現(xiàn)在要把這里面的值跟列的高度做一個(gè)映射鹅颊,值越大高度越高,音頻塊就越多墓造。

val num = (getAvailableHeight() / (danceHeight + danceGap)).toInt()
val b = (mRawAudioBytes[index.toInt()] + 128).toFloat() / 255f
var heightNum =(b * num).toInt()

上面是先得到列最多能展示多少音頻塊堪伍,再根據(jù)mRawAudioBytes的值來(lái)算出當(dāng)前列展示多少個(gè)音頻塊。這一步也叫歸一化觅闽,區(qū)間映射帝雇。

3、繪制每一個(gè)音頻塊

    private fun drawItem(
        heightNum: Int,
        lastDanceRight: Float,
        lastHeight: Float,
        canvas: Canvas?
    ): Float {
        var lastHeight1 = lastHeight
        for (j in 0 until heightNum) {
            mDanceRect.set(
                lastDanceRight,
                lastHeight1 - danceHeight,
                lastDanceRight + danceWidth,
                lastHeight1
            )
            mPaint.shader = null
            if (j >= heightNum - shaderNum) {
                val backGradient = LinearGradient(
                    lastDanceRight,
                    lastHeight1 - danceHeight,
                    lastDanceRight + danceWidth,
                    lastHeight1,
                    intArrayOf(colorStart, colorCenter, colorEnd),
                    null,
                    Shader.TileMode.CLAMP
                )
                mPaint.shader = backGradient
            }
            canvas?.drawRoundRect(mDanceRect, 8f, 8f, mPaint)
            lastHeight1 -= (danceHeight + danceGap)
        }
        return lastHeight1
    }

就是根據(jù)高度來(lái)繪制rectangle蛉拙,算出一列能繪制多少個(gè)音頻塊尸闸,每一個(gè)音頻塊是一個(gè)rectangle,然后繪制rectangle孕锄,為了效果更好吮廉,判斷上面的音頻塊加上漸變。

github地址

歡迎點(diǎn)贊收藏畸肆,后期會(huì)優(yōu)化
https://github.com/hankinghu/AudioVisulizer

使用方法

<com.masoudss.lib.DanceView
              android:id="@+id/danceView"
              android:layout_width="320dp"
              android:layout_height="300dp"
              android:layout_gravity="center"
              app:color_center="@color/red"
              app:color_end="@color/white"
              app:color_start="@color/yellow"
              app:dance_color="@color/yellow"
              app:dance_corner_radius="2dp"
              app:dance_gap="2dp"
              app:max_dance_num="30"
              app:min_dance_num="2"
              app:shader_num="3" />
  • shader_num 頂部加漸變的個(gè)數(shù)
  • color_end 漸變尾部顏色
  • color_start 漸變開(kāi)頭顏色
  • color_center 漸變中間顏色
  • min_dance_num 每一列中最少顯示的個(gè)數(shù)
  • max_dance_num 每一列中最大顯示的個(gè)數(shù)
  • dance_gap 每一個(gè)音頻格之間的間距
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宦芦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子恼除,更是在濱河造成了極大的恐慌踪旷,老刑警劉巖曼氛,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異令野,居然都是意外死亡舀患,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)气破,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)聊浅,“玉大人,你說(shuō)我怎么就攤上這事现使〉统祝” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵碳锈,是天一觀的道長(zhǎng)顽冶。 經(jīng)常有香客問(wèn)我,道長(zhǎng)售碳,這世上最難降的妖魔是什么强重? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮贸人,結(jié)果婚禮上间景,老公的妹妹穿的比我還像新娘。我一直安慰自己艺智,他們只是感情好倘要,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著十拣,像睡著了一般封拧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上父晶,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天哮缺,我揣著相機(jī)與錄音,去河邊找鬼甲喝。 笑死尝苇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的埠胖。 我是一名探鬼主播糠溜,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼直撤!你這毒婦竟也來(lái)了非竿?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤谋竖,失蹤者是張志新(化名)和其女友劉穎红柱,沒(méi)想到半個(gè)月后承匣,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锤悄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年韧骗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片零聚。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡袍暴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出隶症,到底是詐尸還是另有隱情政模,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布蚂会,位于F島的核電站淋样,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏胁住。R本人自食惡果不足惜习蓬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望措嵌。 院中可真熱鬧,春花似錦芦缰、人聲如沸企巢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)浪规。三九已至,卻和暖如春探孝,著一層夾襖步出監(jiān)牢的瞬間笋婿,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工顿颅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缸濒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓粱腻,卻偏偏與公主長(zhǎng)得像庇配,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绍些,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354