Android仿網易雲鯨雲音效動效

20181015183605uYomUVM3iB9s5eJK.jpg
最近網易雲音樂出了一個叫 鯨雲音效 東西,效果怎麼樣不是很清楚,但是播放介面還帶了動效,這個就比較炫酷了,感覺比較有意思,所以也想自己做一個,其中一個我覺得比較好看的效果如下(動圖的來源也比較有意思,後面會講)

G_1107000114.gif-2652.9kB
具體思路
首先自定義佈局是瞭解的,可能會用到 surfaceView
去繪製,整個動畫可以分為四個部分,第一個是旋轉的圖片,這個好說;第二個是運動並且透明度漸變的三角形,這個畫畫也簡單;第三個是根據音樂變化而變化的一個曲線吧,這個可能比較難,我也沒接觸過,不過可以試試看,第四個是模糊的背景,這個簡單。
具體實現
實現模糊的背景
這個倒是簡單,之前也用過一個模糊背景的工具還不錯,不過存在一個問題,我是打算自定義一個 surfaceView
,給 surfaceView
畫一個背景倒是不難,也遇到兩個問題
1.怎麼將圖片以類似自動裁剪居中的方式畫上去,這個想想其實簡單,取得畫布的大小和bitmap的大小,滿足一邊進行縮放,裁剪掉多餘部分就好了
/** * 裁剪圖片 * * @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.由於我後面畫三角形必須得不停地重新整理,背景需要重複繪製,感覺有點浪費資源,看了一下區域性重新整理什麼的感覺沒什麼用,所以就直接先設定為父佈局的普通的背景好了,再將surfaceView設定為透明
@Override public void surfaceCreated(SurfaceHolder surfaceHolder) { setZOrderOnTop(true); getHolder().setFormat(PixelFormat.TRANSLUCENT); }
Android圖片模糊的工具類: ofollow,noindex">https://www.jianshu.com/p/c676fc51f3ef
實現旋轉的圖片
這個更簡單,為了方便也是直接使用一個 ImageView ,通過自帶的檢視裁剪工具剪裁為圓形,然後通過屬性動畫來旋轉
設定一直旋轉的屬性動畫
objectAnimator = ObjectAnimator.ofFloat(ivShowPic, "rotation", 0f, 360f); objectAnimator.setDuration(20 * 1000); objectAnimator.setRepeatMode(ValueAnimator.RESTART); objectAnimator.setInterpolator(new LinearInterpolator()); objectAnimator.setRepeatCount(-1); objectAnimator.start();
檢視裁剪
ivShowPic.setClipToOutline(true); //小小的封裝了一下 ivShowPic.setOutlineProvider(ImageUtil.getOutline(true, 20, 1));
實現運動的三角形
為了保證效能,這個就得使用 surfaceView 來做了;大體思路就是隨機生成一些三角形,三角形速度大小一樣,方向隨機,從圓中心向外移動,移動過程將透明度減小到零
三角形有速度不過速度大小都一樣就先不用管,有速度方向用角度來代替,也好計算運動後的位置,有三個頂點座標。
所以三角形的初步定義
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(); } }
隨機生成了三角形
簡單的方法,就是先指定一個座標區域比如 x 和 y 從-50到50的這個矩形座標區域內,隨機取點,如果構成三角形就為一個隨機三角形,到時候移到中心處只需要x和y座標各加長寬的一半就好了,方向也是-180度到180度取隨機數,便於到時候用 斜率 計算移動後的位置
畫三角形
自定義surfaceView的通用寫法都一樣,隨便看一下文章
Android中的SurfaceView詳解: https://www.jianshu.com/p/b037249e6d31
我們先清空畫布,然後可以隨機生成一些三角形,儲存所有生成的三角形到一個集合裡面,然後設定一個速度,根據每個三角形的方向來計算距離上一次重新整理移動到了哪個位置,通過位置計算與中心點的距離來設定透明度,然後畫上去
//三角形移動速度 private double moveSpeed = 0.4; //重新整理時間 private static int refreshTime = 20; //新增兩次三角形的間隔 private static int addTriangleInterval = 100; //每次新增的數量限制 private static int addTriangleOnece = 2; //總三角形數量 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);
具體程式碼看專案原始碼,這裡注意需要設定幾個值來調整動畫效果到最佳,做的過程中也有出現一些很魔性的動畫,很有意思
然後發現, surfaceView 的動畫會出現在 imageView 的上面,雖然我把 imageView 的高度調了一下還是沒效果,發現是之前設定surfaceView透明的時候 setZOrderOnTop(true)
導致的問題;但是如果不設定 surfaceView 又會遮擋背景,的確是沒好辦法解決
其實可以簡單點,判斷三角形的移動距離小於 imageView 的時候設定全透明就好了,做出來大概是這樣的效果:

G_1107013001.gif-2394.6kB
視訊效果: http://oy5r220jg.bkt.clouddn.com/record__1107012332_1.mp4
其實還是有一點問題的,可以把 Imageview 的旋轉在 surfaceView 裡面實現,這個應該三角形的出現可以會自然一點,其他解決辦法倒是暫時沒想到
優化
為了讓三角形出現自然一點,可以把 Imageview 的旋轉在 surfaceView 裡面實現,但是好像不好做,因為還得裁剪圖片和控制旋轉,相比 imageView 來實現我覺得稍微有點麻煩了;那還可以不設定 setZOrderOnTop(true)
,這樣背景變成了黑色,還需要畫一個背景上去;
那麼兩種方法比較一下,其實模糊化以後的背景質量非常小(圖片都模糊了肯定小呀),遠遠小於要旋轉的那張圖片的質量,所以繪製 surfaceView 背景可能比較好;
獲取控制元件的截圖
由於我的 surfaceView 不是寬高全屏的,只是中間一部分,而且給 surfaceView 設定的背景圖片肯定要和整個佈局的背景重合,可以先獲取背景檢視的截圖,然後在這裡面裁剪出 surfaceView 所在區域
//啟用DrawingCache並建立點陣圖 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); //關閉DrawingCache iv_bg.setDrawingCacheEnabled(false);
為什麼要先獲取背景檢視的截圖,而不直接用那個模糊化的圖片呢,因為模糊化的圖片尺寸超級小,顯示的時候被放大了,而且可能還被裁剪了(背景用的imageView顯示的),為保證裁剪後和背景重合還得做很多圖象處理,還是直接獲取截圖來的簡單
動態獲取顏色
關於三角形的顏色,其實也是要根據背景來設定的
Material Design鼓勵使用動態顏色,新的 Palette 支援庫可以提取圖片中的一部分顏色來設定你的UI的樣式來使介面顏色互相搭配以提供一種沉浸式體驗。提取出來的調色盤(palette)包括突出的和柔和的色調
Vibrant (有活力)
Vibrant dark(有活力 暗色)
Vibrant light(有活力 亮色)
Muted (柔和)
Muted dark(柔和 暗色)
Muted light(柔和 亮色)
就是可以從bitmap中獲取幾種特殊的顏色,注意獲取到的swatche可能為空的
// Palette的部分 Palette palette = Palette.generate(bitmap); Palette.Swatch swatche = null; //獲取不同風格的顏色, swatche = palette.getVibrantSwatch(); swatche = palette.getLightVibrantSwatch(); swatche = palette.getDarkVibrantSwatch(); swatche = palette.getMutedSwatch(); //我用這個和網易雲接近,其他顏色也都挺漂亮 swatche = palette.getLightMutedSwatch(); swatche = palette.getDarkMutedSwatch(); swatche = palette.getVibrantSwatch(); //獲取顏色 int color = swatche.getRgb();
視訊效果: http://oy5r220jg.bkt.clouddn.com/record__1107120942_1.mp4
換個顏色: http://oy5r220jg.bkt.clouddn.com/record__1107121058_1.mp4

未標題-1.png
改變圖片的亮度
但是發現一個問題,背景顏色太亮了,我選擇 palette.getLightMutedSwatch()
是最亮的顏色,還是會被背景干擾,這個設定最上層的佈局背景為半透明,發現我 surfaceView 也跟著被半透明覆蓋了呀,如果只覆蓋背景的話, surfaceView 繪製的背景是從作為背景的 ImageVIew 擷取的圖片,會和背景顏色不一樣的,只能從背景ImageView入手,還真的有改變亮度的辦法,不僅可以改變 亮度 ,還可以改變 色相 和 飽和度
ColorMatrix colorMatrix = new ColorMatrix(); //改變圖片亮度 colorMatrix.setScale(0.5f,0.5f,0.5f,1); ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(colorMatrix); iv_bg.setColorFilter(colorFilter);
改變了亮度後對動態獲取顏色會有影響,亮色的可能獲取不到了,獲取顏色應該提前獲取
開始畫線
仔細看了一下,先畫圍繞這個圓畫很多點,隔一段一個點,然後把點用曲線圈起來就ok了,動的時候就是設定一個上下移動的距離,一個點變成兩個,兩個點先連線,然後同一側的點重新連成曲線,感覺是是這樣的,先試試
圍繞圓畫點
這個就是直線和圓的交點問題,從-180度到180度,每間隔一個角度,取斜率計算交點,差不多是這個意思
y = (Math.sin(angle) * circleR); x = (Math.cos(angle) * circleR);

未標題-1.png
畫出來一看,這是什麼情況,根本不均勻,沒道理呀,原來是 Math.sin(angle)
和 Math.cos(angle)
裡面的值指的是弧度,不是角度,所以轉換一下
y = (Math.sin(Math.toRadians(angle)) * circleR); x = (Math.cos(Math.toRadians(angle)) * circleR);
畫貝塞爾曲線
我先用二階貝塞爾曲線把相鄰的點連了起來,中間的點取的是兩個點的圓弧中間的點,反正看起來是一個圓
Path path = new Path(); path.moveTo(point.x, point.y); //畫二階貝塞爾曲線 path.quadTo(bezierPoint.x, bezierPoint.y, next.x, next.y); canvas.drawPath(path, paint);
原理如下圖

二階貝塞爾曲線
處理點的跳動
到了最後一步,讓點分裂成兩個分別上下移動後,再次將同一邊的連成曲線並將移動後的上下兩個點連線,移動距離先取隨機數,效果好了再看音訊相關東西,這個有點難度,我嘗試了很多次,都不是我想要的結果

未標題-3.png
看起來都失敗了,感覺這個移動距離不能取隨機數,最後一個看起來比較像是手動輸入了一組均勻的資料,並且是直接畫的直線
獲取音訊資訊
感覺模擬資料不行,還是先看看怎麼獲取音訊資訊;獲取音訊資訊比較簡單
1.使用MediaPlayer播放傳入的音樂,並拿到mediaPlayerId
2.使用Visualizer類拿到拿到MediaPlayer播放中的音訊資料(wave/fft)
3.將資料用自定義控制元件展現出來
使用Visualizer需要錄音的動態許可權, 如果播放sd卡音訊需要STORAGE許可權
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
播放音樂
MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.music_wheresilove); mediaPlayer.setLooping(true); mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { mediaPlayer.start(); } });
Visualizer回撥
Visualizer.OnDataCaptureListener
有2個回撥,一個用於顯示FFT資料,展示不同頻率的振幅,另一個用於顯示聲音的波形圖
private Visualizer.OnDataCaptureListener dataCaptureListener = new Visualizer.OnDataCaptureListener() { @Override public void onWaveFormDataCapture(Visualizer visualizer, final byte[] waveform, int samplingRate) { //到waveform為波形圖資料 } @Override public void onFftDataCapture(Visualizer visualizer, final byte[] fft, int samplingRate) { //FFT資料,展示不同頻率的振幅 } };
Visualizer有兩個比較重要的引數
設定視覺化資料的資料大小 範圍[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange() 1 ]
設定視覺化資料的採集頻率 範圍[0~Visualizer.getMaxCaptureRate()]
visualizer = new Visualizer(mediaPlayer.getAudioSessionId()); //取樣的最大值 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);
這樣紙我們就拿到了兩組資料,波形圖和頻譜圖,很顯然頻譜圖是展示不同頻率的振幅的,一般情況下只有少部分頻率會變動,所以我選擇波形圖。
拿到的波形圖是一個byte陣列,裡面也是類似每個點的振幅,我們把數組裡的資料作為高度畫一條線,排成一排正常畫出來
//畫音訊線 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); }
效果是這樣紙,用另一個頻譜圖也差不多,就是變化的區域有點少

未標題-2.png-159.6kB
這樣紙的話,那是不是我把它繞圓一圈,然後在按相反方向繞一圈,同樣跳動的兩個點連線,然後隨便畫畫曲線是不是就ok啦;做完就發現裡面的值太大了,都看不出來是個圓了,那就都減去一點高度什麼的,調整一下大小;然後這次就先畫一個三次貝塞爾曲線吧,畫出來跟跟屎一樣,這個曲線是真的難畫呀,而且畫的慢,看起來不是很流暢;我再次嘗試用簡單的方法畫
折線的頂點時候用圓角,並沒有什麼亂用
mPaint.setStrokeJoin(Paint.Join.ROUND);
設定path中的連線處有個角度,看起來接近了一些,不過還是差很遠
CornerPathEffect cornerPathEffect = new CornerPathEffect(130); mPaint.setPathEffect(cornerPathEffect);
視訊效果: http://ozr6klu3a.bkt.clouddn.com/record__1110000510.mp4
感覺必須先對資料進行處理才能得到想要的效果
未完待續...
視訊轉Gif工具實現: https://www.jianshu.com/p/81cb36b610f4
視訊的裁剪其實也是上面這個專案的程式碼,但是暫時沒有做功能,會更新