Android 仿网易云鲸云音效动效
code小生,一個(gè)專(zhuān)注 Android 領(lǐng)域的技術(shù)平臺(tái)
公眾號(hào)回復(fù) Android 加入我的安卓技術(shù)群
作者:Tyhj
鏈接:https://www.jianshu.com/p/d2996afeb3e1
聲明:本文已獲Tyhj授權(quán)發(fā)表,轉(zhuǎn)發(fā)等請(qǐng)聯(lián)系原作者授權(quán)
最近網(wǎng)易云音樂(lè)出了一個(gè)叫鯨云音效東西,效果怎么樣不是很清楚,但是播放界面還帶了動(dòng)效,這個(gè)就比較炫酷了,感覺(jué)比較有意思,所以也想自己做一個(gè),其中一個(gè)我覺(jué)得比較好看的效果如下(動(dòng)圖的來(lái)源也比較有意思,后面會(huì)講)
具體思路
首先自定義布局是了解的,可能會(huì)用到 surfaceView 去繪制,整個(gè)動(dòng)畫(huà)可以分為四個(gè)部分,第一個(gè)是旋轉(zhuǎn)的圖片,這個(gè)好說(shuō);第二個(gè)是運(yùn)動(dòng)并且透明度漸變的三角形,這個(gè)畫(huà)畫(huà)也簡(jiǎn)單;第三個(gè)是根據(jù)音樂(lè)變化而變化的一個(gè)曲線吧,這個(gè)可能比較難,我也沒(méi)接觸過(guò),不過(guò)可以試試看,第四個(gè)是模糊的背景,這個(gè)簡(jiǎn)單。
具體實(shí)現(xiàn)
實(shí)現(xiàn)模糊的背景
這個(gè)倒是簡(jiǎn)單,之前也用過(guò)一個(gè)模糊背景的工具還不錯(cuò),不過(guò)存在一個(gè)問(wèn)題,我是打算自定義一個(gè) surfaceView,給 surfaceView 畫(huà)一個(gè)背景倒是不難,也遇到兩個(gè)問(wèn)題
1.怎么將圖片以類(lèi)似自動(dòng)裁剪居中的方式畫(huà)上去,這個(gè)想想其實(shí)簡(jiǎn)單,取得畫(huà)布的大小和bitmap的大小,滿足一邊進(jìn)行縮放,裁剪掉多余部分就好了
/**?????*?裁剪圖片
?????*
?????*?@param?rectBitmap
?????*?@param?rectSurface
?????*/
????public?static?void?centerCrop(Rect?rectBitmap,?Rect?rectSurface)?{
????????int?verticalTimes?=?rectBitmap.height()?/?rectSurface.height();
????????int?horizontalTimes?=?rectBitmap.width()?/?rectSurface.width();
????????if?(verticalTimes?>?horizontalTimes)?{
????????????rectBitmap.left?=?0;
????????????rectBitmap.right?=?rectBitmap.right;
????????????rectBitmap.top?=?(rectBitmap.height()?-?(rectSurface.height()?*?rectBitmap.width()?/?rectSurface.width()))?/?2;
????????????rectBitmap.bottom?=?rectBitmap.bottom?-?rectBitmap.top;
????????}?else?{
????????????rectBitmap.top?=?0;
????????????rectBitmap.bottom?=?rectBitmap.bottom;
????????????rectBitmap.left?=?(rectBitmap.width()?-?(rectSurface.width()?*?rectBitmap.height()?/?rectSurface.height()))?/?2;
????????????rectBitmap.right?=?rectBitmap.right?-?rectBitmap.left;
????????}
????}
2.由于我后面畫(huà)三角形必須得不停地刷新,背景需要重復(fù)繪制,感覺(jué)有點(diǎn)浪費(fèi)資源,看了一下局部刷新什么的感覺(jué)沒(méi)什么用,所以就直接先設(shè)置為父布局的普通的背景好了,再將 surfaceView 設(shè)置為透明
public?void?surfaceCreated(SurfaceHolder?surfaceHolder)?
{
????setZOrderOnTop(true);
????getHolder().setFormat(PixelFormat.TRANSLUCENT);
}
Android 圖片模糊的工具類(lèi):
https://www.jianshu.com/p/c676fc51f3ef
實(shí)現(xiàn)旋轉(zhuǎn)的圖片
這個(gè)更簡(jiǎn)單,為了方便也是直接使用一個(gè) ImageView,通過(guò)自帶的視圖裁剪工具剪裁為圓形,然后通過(guò)屬性動(dòng)畫(huà)來(lái)旋轉(zhuǎn)
設(shè)置一直旋轉(zhuǎn)的屬性動(dòng)畫(huà)
objectAnimator?=?ObjectAnimator.ofFloat(ivShowPic,?"rotation",?0f,?360f);objectAnimator.setDuration(20?*?1000);
objectAnimator.setRepeatMode(ValueAnimator.RESTART);
objectAnimator.setInterpolator(new?LinearInterpolator());
objectAnimator.setRepeatCount(-1);
objectAnimator.start();
視圖裁剪
/**?????*?設(shè)置裁剪為圓形
?????*
?????*?@param?view
?????*?@param?pading??這個(gè)是設(shè)置間距是長(zhǎng)或?qū)挼膸追种?br />?????*/
????(api?=?Build.VERSION_CODES.LOLLIPOP)
????public?static?void?setCircleShape(View?view,?final?int?pading)?{
????????view.setClipToOutline(true);
????????view.setOutlineProvider(new?ViewOutlineProvider()?{
????????????
????????????public?void?getOutline(View?view,?Outline?outline)?{
????????????????int?margin?=?Math.min(view.getWidth(),?view.getHeight())?/?pading;
????????????????outline.setOval(margin,?margin,?view.getWidth()?-?margin,?view.getHeight()?-?margin);
????????????}
????????});
????}
實(shí)現(xiàn)運(yùn)動(dòng)的三角形
為了保證性能,這個(gè)就得使用 surfaceView 來(lái)做了;大體思路就是隨機(jī)生成一些三角形,三角形速度大小一樣,方向隨機(jī),從圓中心向外移動(dòng),移動(dòng)過(guò)程將透明度減小到零
三角形有速度不過(guò)速度大小都一樣就先不用管,有速度方向用角度來(lái)代替,也好計(jì)算運(yùn)動(dòng)后的位置,有三個(gè)頂點(diǎn)坐標(biāo)。
所以三角形的初步定義
public?class?Triangle?{????public?Point?topPoint1,?topPoint2,?topPoint3;
????public?int?moveAngle;
????public?Triangle(Point?topPoint1,?Point?topPoint2,?Point?topPoint3)?{
????????this.topPoint1?=?topPoint1;
????????this.topPoint2?=?topPoint2;
????????this.topPoint3?=?topPoint3;
????????moveAngle?=?getMoveAngel();
????}
}
隨機(jī)生成了三角形
簡(jiǎn)單的方法,就是先指定一個(gè)坐標(biāo)區(qū)域比如x和y從-50到50的這個(gè)矩形坐標(biāo)區(qū)域內(nèi),隨機(jī)取點(diǎn),如果構(gòu)成三角形就為一個(gè)隨機(jī)三角形,到時(shí)候移到中心處只需要x和y坐標(biāo)各加長(zhǎng)寬的一半就好了,方向也是-180度到180度取隨機(jī)數(shù),便于到時(shí)候用斜率計(jì)算移動(dòng)后的位置
畫(huà)三角形
自定義 surfaceView 的通用寫(xiě)法都一樣,隨便看一下文章
Android中的 SurfaceView 詳解:
https://www.jianshu.com/p/b037249e6d31
我們先清空畫(huà)布,然后可以隨機(jī)生成一些三角形,保存所有生成的三角形到一個(gè)集合里面,然后設(shè)定一個(gè)速度,根據(jù)每個(gè)三角形的方向來(lái)計(jì)算距離上一次刷新移動(dòng)到了哪個(gè)位置,通過(guò)位置計(jì)算與中心點(diǎn)的距離來(lái)設(shè)置透明度,然后畫(huà)上去
//三角形移動(dòng)速度private?double?moveSpeed?=?0.4;
//刷新時(shí)間
private?static?int?refreshTime?=?20;
//添加兩次三角形的間隔
private?static?int?addTriangleInterval?=?100;
//每次添加的數(shù)量限制
private?static?int?addTriangleOnece?=?2;
//總?cè)切螖?shù)量
private?int?allTriangleCount?=?100;
mCanvas?=?mSurfaceHolder.lockCanvas();
mCanvas.drawColor(0,?PorterDuff.Mode.CLEAR);
manageTriangle((int)?(refreshTime?*?moveSpeed));
for?(Triangle?triangle?:?triangleList)?{
????drawTriangle(mCanvas,?triangle,?mPaintColor);
}
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
Thread.sleep(refreshTime);
具體代碼看項(xiàng)目源碼,這里注意需要設(shè)定幾個(gè)值來(lái)調(diào)整動(dòng)畫(huà)效果到最佳,做的過(guò)程中也有出現(xiàn)一些很魔性的動(dòng)畫(huà),很有意思
然后發(fā)現(xiàn),surfaceView 的動(dòng)畫(huà)會(huì)出現(xiàn)在 imageView 的上面,雖然我把imageView 的高度調(diào)了一下還是沒(méi)效果,發(fā)現(xiàn)是之前設(shè)置 surfaceView 透明的時(shí)候 setZOrderOnTop(true) 導(dǎo)致的問(wèn)題;但是如果不設(shè)置 surfaceView 又會(huì)遮擋背景,的確是沒(méi)好辦法解決
其實(shí)可以簡(jiǎn)單點(diǎn),判斷三角形的移動(dòng)距離小于 imageView 的時(shí)候設(shè)置全透明就好了,做出來(lái)大概是這樣的效果:
視頻效果:http://oy5r220jg.bkt.clouddn.com/record__1107012332_1.mp4
其實(shí)還是有一點(diǎn)問(wèn)題的,可以把 Imageview 的旋轉(zhuǎn)在 surfaceView 里面實(shí)現(xiàn),這個(gè)應(yīng)該三角形的出現(xiàn)可以會(huì)自然一點(diǎn),其他解決辦法倒是暫時(shí)沒(méi)想到
優(yōu)化
為了讓三角形出現(xiàn)自然一點(diǎn),可以把 Imageview 的旋轉(zhuǎn)在 surfaceView 里面實(shí)現(xiàn),但是好像不好做,因?yàn)檫€得裁剪圖片和控制旋轉(zhuǎn),相比 imageView 來(lái)實(shí)現(xiàn)我覺(jué)得稍微有點(diǎn)麻煩了;那還可以不設(shè)置 setZOrderOnTop(true),這樣背景變成了黑色,還需要畫(huà)一個(gè)背景上去;
那么兩種方法比較一下,其實(shí)模糊化以后的背景質(zhì)量非常小(圖片都模糊了肯定小呀),遠(yuǎn)遠(yuǎn)小于要旋轉(zhuǎn)的那張圖片的質(zhì)量,所以繪制surfaceView背景可能比較好;
獲取控件的截圖
由于我的 surfaceView 不是寬高全屏的,只是中間一部分,而且給surfaceView 設(shè)置的背景圖片肯定要和整個(gè)布局的背景重合,可以先獲取背景視圖的截圖,然后在這里面裁剪出 surfaceView 所在區(qū)域
//啟用DrawingCache并創(chuàng)建位圖iv_bg.setDrawingCacheEnabled(true);
iv_bg.buildDrawingCache();
//獲取bitmap
Bitmap?bitmap2?=?Bitmap.createBitmap(iv_bg.getDrawingCache());
//裁剪
bitmap2?=?Bitmap.createBitmap(bitmap2,?0,?jinyunView.getTop(),?jinyunView.getWidth(),?jinyunView.getHeight());
//bitmap2傳給surfaceView
jinyunView.setBitmapBg(bitmap2);
//關(guān)閉DrawingCache
iv_bg.setDrawingCacheEnabled(false);
為什么要先獲取背景視圖的截圖,而不直接用那個(gè)模糊化的圖片呢,因?yàn)槟:膱D片尺寸超級(jí)小,顯示的時(shí)候被放大了,而且可能還被裁剪了(背景用的imageView顯示的),為保證裁剪后和背景重合還得做很多圖象處理,還是直接獲取截圖來(lái)的簡(jiǎn)單
動(dòng)態(tài)獲取顏色
關(guān)于三角形的顏色,其實(shí)也是要根據(jù)背景來(lái)設(shè)定的
Material Design 鼓勵(lì)使用動(dòng)態(tài)顏色,新的 Palette 支持庫(kù)可以提取圖片中的一部分顏色來(lái)設(shè)置你的 UI 的樣式來(lái)使界面顏色互相搭配以提供一種沉浸式體驗(yàn)。提取出來(lái)的調(diào)色板(palette)包括突出的和柔和的色調(diào)
Vibrant (有活力)
Vibrant dark(有活力 暗色)
Vibrant light(有活力 亮色)
Muted (柔和)
Muted dark(柔和 暗色)
Muted light(柔和 亮色)
就是可以從bitmap中獲取幾種特殊的顏色,注意獲取到的swatche可能為空的
//?Palette的部分Palette?palette?=?Palette.generate(bitmap);
Palette.Swatch?swatche?=?null;
//獲取不同風(fēng)格的顏色,
swatche?=?palette.getVibrantSwatch();
swatche?=?palette.getLightVibrantSwatch();
swatche?=?palette.getDarkVibrantSwatch();
swatche?=?palette.getMutedSwatch();
//我用這個(gè)和網(wǎng)易云接近,其他顏色也都挺漂亮
swatche?=?palette.getLightMutedSwatch();
swatche?=?palette.getDarkMutedSwatch();
swatche?=?palette.getVibrantSwatch();
//獲取顏色
int?color?=?swatche.getRgb();
視頻效果:http://lc-fgtnb2h8.cn-n1.lcfile.com/7f08b2eea6a4039cf453.mp4
換個(gè)顏色:http://lc-fgtnb2h8.cn-n1.lcfile.com/45e70109d2cbc9b7371b.mp4
改變圖片的亮度
但是發(fā)現(xiàn)一個(gè)問(wèn)題,背景顏色太亮了,我選擇palette.getLightMutedSwatch() 是最亮的顏色,還是會(huì)被背景干擾,這個(gè)設(shè)置最上層的布局背景為半透明,發(fā)現(xiàn)我 surfaceView 也跟著被半透明覆蓋了呀,如果只覆蓋背景的話,surfaceView 繪制的背景是從作為背景的ImageVIew 截取的圖片,會(huì)和背景顏色不一樣的,只能從背景 ImageView 入手,還真的有改變亮度的辦法,不僅可以改變亮度,還可以改變色相和飽和度
ColorMatrix?colorMatrix?=?new?ColorMatrix();//改變圖片亮度
colorMatrix.setScale(0.5f,0.5f,0.5f,1);
ColorMatrixColorFilter?colorFilter?=?new?ColorMatrixColorFilter(colorMatrix);
iv_bg.setColorFilter(colorFilter);
改變了亮度后對(duì)動(dòng)態(tài)獲取顏色會(huì)有影響,亮色的可能獲取不到了,獲取顏色應(yīng)該提前獲取
開(kāi)始畫(huà)線
仔細(xì)看了一下,先畫(huà)圍繞這個(gè)圓畫(huà)很多點(diǎn),隔一段一個(gè)點(diǎn),然后把點(diǎn)用曲線圈起來(lái)就ok了,動(dòng)的時(shí)候就是設(shè)置一個(gè)上下移動(dòng)的距離,一個(gè)點(diǎn)變成兩個(gè),兩個(gè)點(diǎn)先連線,然后同一側(cè)的點(diǎn)重新連成曲線,感覺(jué)是是這樣的,先試試
圍繞圓畫(huà)點(diǎn)
這個(gè)就是直線和圓的交點(diǎn)問(wèn)題,從-180度到180度,每間隔一個(gè)角度,取斜率計(jì)算交點(diǎn),差不多是這個(gè)意思
y?=?(Math.sin(angle)?*?circleR);x?=?(Math.cos(angle)?*?circleR);
畫(huà)出來(lái)一看,這是什么情況,根本不均勻,沒(méi)道理呀,原來(lái)是 Math.sin(angle) 和 Math.cos(angle) 里面的值指的是弧度,不是角度,所以轉(zhuǎn)換一下
y?=?(Math.sin(Math.toRadians(angle))?*?circleR);x?=?(Math.cos(Math.toRadians(angle))?*?circleR);
畫(huà)貝塞爾曲線
我先用二階貝塞爾曲線把相鄰的點(diǎn)連了起來(lái),中間的點(diǎn)取的是兩個(gè)點(diǎn)的圓弧中間的點(diǎn),反正看起來(lái)是一個(gè)圓
Path?path?=?new?Path();path.moveTo(point.x,?point.y);
//畫(huà)二階貝塞爾曲線
path.quadTo(bezierPoint.x,?bezierPoint.y,?next.x,?next.y);
canvas.drawPath(path,?paint);
原理如下圖
二階貝塞爾曲線
處理點(diǎn)的跳動(dòng)
到了最后一步,讓點(diǎn)分裂成兩個(gè)分別上下移動(dòng)后,再次將同一邊的連成曲線并將移動(dòng)后的上下兩個(gè)點(diǎn)連線,移動(dòng)距離先取隨機(jī)數(shù),效果好了再看音頻相關(guān)東西,這個(gè)有點(diǎn)難度,我嘗試了很多次,都不是我想要的結(jié)果
未標(biāo)題-3.png看起來(lái)都失敗了,感覺(jué)這個(gè)移動(dòng)距離不能取隨機(jī)數(shù),最后一個(gè)看起來(lái)比較像是手動(dòng)輸入了一組均勻的數(shù)據(jù),并且是直接畫(huà)的直線
獲取音頻信息
感覺(jué)模擬數(shù)據(jù)不行,還是先看看怎么獲取音頻信息;獲取音頻信息比較簡(jiǎn)單
1.使用MediaPlayer播放傳入的音樂(lè),并拿到mediaPlayerId
2.使用Visualizer類(lèi)拿到拿到MediaPlayer播放中的音頻數(shù)據(jù)(wave/fft)
3.將數(shù)據(jù)用自定義控件展現(xiàn)出來(lái)
使用Visualizer需要錄音的動(dòng)態(tài)權(quán)限, 如果播放sd卡音頻需要STORAGE權(quán)限
<uses-permission?android:name="android.permission.RECORD_AUDIO"?/><uses-permission?android:name="android.permission.WRITE_EXTERNAL_STORAGE"?/>
播放音樂(lè)
MediaPlayer?mediaPlayer?=?MediaPlayer.create(this,?R.raw.music_wheresilove);mediaPlayer.setLooping(true);
mediaPlayer.setOnPreparedListener(new?MediaPlayer.OnPreparedListener()?{
????
????public?void?onPrepared(MediaPlayer?mediaPlayer)?{
????????mediaPlayer.start();
????}
});
Visualizer回調(diào)
Visualizer.OnDataCaptureListener 有2個(gè)回調(diào),一個(gè)用于顯示FFT數(shù)據(jù),展示不同頻率的振幅,另一個(gè)用于顯示聲音的波形圖
private?Visualizer.OnDataCaptureListener?dataCaptureListener?=?new?Visualizer.OnDataCaptureListener()?{????????
????????public?void?onWaveFormDataCapture(Visualizer?visualizer,?final?byte[]?waveform,?int?samplingRate)?{
????????????//到waveform為波形圖數(shù)據(jù)
????????}
????????
????????public?void?onFftDataCapture(Visualizer?visualizer,?final?byte[]?fft,?int?samplingRate)?{
????????????//FFT數(shù)據(jù),展示不同頻率的振幅
????????}
????};
Visualizer 有兩個(gè)比較重要的參數(shù)
設(shè)置可視化數(shù)據(jù)的數(shù)據(jù)大小 范圍[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange()1]
設(shè)置可視化數(shù)據(jù)的采集頻率 范圍[0~Visualizer.getMaxCaptureRate()]
//采樣的最大值
int?captureSize?=?Visualizer.getCaptureSizeRange()[1];
?//采樣的頻率
int?captureRate?=?Visualizer.getMaxCaptureRate()?*?3?/?4;
visualizer.setCaptureSize(captureSize);
visualizer.setDataCaptureListener(dataCaptureListener,?captureRate,?true,?true);
visualizer.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED);
visualizer.setEnabled(true);
有一個(gè)很有意思的地方,如果audioSessionId設(shè)置為零,就直接獲取系統(tǒng)的音頻,這個(gè)很有意思,連蒙帶猜搞出來(lái)的
visualizer?=?new?Visualizer(0);這樣紙我們就拿到了兩組數(shù)據(jù),波形圖和頻譜圖,很顯然頻譜圖是展示不同頻率的振幅的,一般情況下只有少部分頻率會(huì)變動(dòng),所以我選擇波形圖。
拿到的波形圖是一個(gè)byte數(shù)組,里面也是類(lèi)似每個(gè)點(diǎn)的振幅,我們把數(shù)組里的數(shù)據(jù)作為高度畫(huà)一條線,排成一排正常畫(huà)出來(lái)
?//畫(huà)音頻線????private?void?drawAudioLine(Canvas?canvas)?{
????????if?(mPoints?==?null?||?mPoints.length?<?mBytes.length?*?4)?{
????????????mPoints?=?new?float[mBytes.length?*?4];
????????}
????????for?(int?i?=?1;?i?<?pointSize;?i++)?{
????????????if?(mBytes[i]?<?0)?{
????????????????mBytes[i]?=?127;
????????????}
????????????mPoints[i?*?4]?=?getWidth()?*?i?/?pointSize;
????????????mPoints[i?*?4?+?1]?=?getHeight()?/?2;
????????????mPoints[i?*?4?+?2]?=?getWidth()?*?i?/?pointSize;
????????????mPoints[i?*?4?+?3]?=?2?+?getHeight()?/?2?-?mBytes[i];
????????}
????????canvas.drawLines(mPoints,?mPaint);
????}
效果是這樣紙,用另一個(gè)頻譜圖也差不多,就是變化的區(qū)域有點(diǎn)少
這樣紙的話,那是不是我把它繞圓一圈,然后在按相反方向繞一圈,同樣跳動(dòng)的兩個(gè)點(diǎn)連線,然后隨便畫(huà)畫(huà)曲線是不是就ok啦;做完就發(fā)現(xiàn)里面的值太大了,都看不出來(lái)是個(gè)圓了,那就都減去一點(diǎn)高度什么的,調(diào)整一下大小;然后這次就先畫(huà)一個(gè)三次貝塞爾曲線吧,畫(huà)出來(lái)跟跟屎一樣,這個(gè)曲線是真的難畫(huà)呀,而且畫(huà)的慢,看起來(lái)不是很流暢;我再次嘗試用簡(jiǎn)單的方法畫(huà)
折線的頂點(diǎn)時(shí)候用圓角,并沒(méi)有什么亂用
mPaint.setStrokeJoin(Paint.Join.ROUND);設(shè)置path中的連接處有個(gè)角度,看起來(lái)接近了一些,不過(guò)還是差很遠(yuǎn)
CornerPathEffect?cornerPathEffect?=?new?CornerPathEffect(130);mPaint.setPathEffect(cornerPathEffect);
視頻效果:http://lc-fgtnb2h8.cn-n1.lcfile.com/fada1f97f943dd6e944d.mp4
其實(shí)可以看出來(lái)做法是沒(méi)有問(wèn)題的,但是必須先對(duì)數(shù)據(jù)進(jìn)行處理才能得到想要的效果,但是具體怎么處理這個(gè)的確需要不斷嘗試;如果處理好可以做出更多更好看的效果;
上面的視頻效果在github的之前的提交版本里,有興趣可以找找,現(xiàn)在在不斷嘗試新的效果,有找到比較好的,會(huì)更新上來(lái);有誰(shuí)搞出炫酷的效果,希望大家不吝賜教
有想法的同學(xué)記得告訴我呀
視頻轉(zhuǎn)Gif工具實(shí)現(xiàn):https://www.jianshu.com/p/81cb36b610f4
視頻的裁剪其實(shí)也是上面這個(gè)項(xiàng)目的代碼,但是暫時(shí)沒(méi)有做功能,會(huì)更新
項(xiàng)目地址:https://github.com/tyhjh/Jinyuneffect
分享技術(shù)我是認(rèn)真的
總結(jié)
以上是生活随笔為你收集整理的Android 仿网易云鲸云音效动效的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 蒙特卡罗方法和拉斯维加斯方法
- 下一篇: vscode通过插件一键运行 c++单元