關於Flutter,以前寫了兩篇文章,第一篇Flutter如何和Native通訊-Android視角簡單說了一下如何使用Flutter和Native的通訊通道:Platform Channels;第二篇Flutter插件(Plugin)開發 - Android視角講了Flutter插件開發的過程,文中咱們把Android MediaPlayer
的部分功能包裝成了個Flutter插件。而且實現了個使用這個插件的低配版音樂播放器。java
爲了繼續學習Flutter開發,順便也想看看Flutter app的性能表現如何,我在這個低配版音樂播放器上加了個音樂柱狀頻譜圖。這篇文章會講講具體怎麼來作這件事。全部代碼都可從Github獲取。先上張動圖你們感覺一下。 android
動圖裏那些動來動去的上紅下綠的柱子就是當前正在播放的音樂的頻譜,從左至右頻率依次升高。接下來咱們來實現這樣的效果吧,首先仍是看看Native端怎麼作。git
頻譜數據是經過Android自帶的Visualizer
獲取的。而要使用Visualizer
首先要取得android.permission.RECORD_AUDIO
權限。咱們要先處理一下插件中動態請求權限的狀況。github
首先在插件的AndroidManifest.xml
中加入android.permission.RECORD_AUDIO
權限。canvas
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.github.zhangjianli.fluttermusicplugin">
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
</manifest>
複製代碼
而後在FlutterMusicPlugin
的構造函數中檢查下權限(不建議這樣作)。app
private FlutterMusicPlugin(Activity activity) {
mActivity = activity;
if (mActivity.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
mActivity.requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSIONS_REQUEST_RECORD_AUDIO);
}
}
複製代碼
動態權限的回調在registerWith
中註冊。ide
public static void registerWith(Registrar registrar) {
final FlutterMusicPlugin plugin = new FlutterMusicPlugin(registrar.activity());
...
registrar.addActivityResultListener(plugin);
...
}
複製代碼
權限回調的處理,簡單起見,這裏咱們直接退出app。函數
@Override
public boolean onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_RECORD_AUDIO :
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
return true;
} else {
mActivity.finish();
return false;
}
default:
return false;
}
}
複製代碼
權限的問題處理好了。接下來咱們要作的就是在本地播放音樂的同時使用Visualizer
來獲取頻譜數據。工具
mMediaPlayer.prepare();
mVisualizer = new Visualizer(mMediaPlayer.getAudioSessionId());
mVisualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[0]);
mVisualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
public void onWaveFormDataCapture(Visualizer visualizer, byte[] bytes, int samplingRate) {
public void onFftDataCapture(Visualizer visualizer, byte[] bytes, int samplingRate) {
// 獲得頻譜數據
byte[] spectrum = new byte[bytes.length / 2];
// 轉換爲幅度
for (int i = 0; i < spectrum.length; i++) {
Double magnitude = Math.hypot(bytes[2*i], bytes[2*i+1]);
if (magnitude < 0) {
spectrum[i] = 0;
} else if (magnitude > 127) {
spectrum[i] = 127 & 0xFF;
} else {
spectrum[i] = magnitude.byteValue();
}
}
//經過EventChannel發送給Flutter
mSpectrumSink.success(spectrum);
}
}, Visualizer.getMaxCaptureRate()/2, false, true);
mVisualizer.setEnabled(true);
mMediaPlayer.start();
複製代碼
獲取到的頻譜數據轉換爲幅度數據之後,經過EventChannel發送給Flutter。 EventChannel的使用可參考Flutter如何和Native通訊-Android視角。這裏再也不重複。post
頻譜柱狀圖的顯示咱們作成了一個Widget,名字叫Visualizer
。因爲頻譜是不停變化的,因此它是一個StatefulWidget
。
class Visualizer extends StatefulWidget {
@override
VisualizerState createState() => VisualizerState();
}
class VisualizerState extends State<Visualizer> {
// 頻譜數據
Uint8List _spectrum;
@override
void initState() {
super.initState();
// connect to native channels
FlutterMusicPlugin.listenSpectrum(_onSpectrum, _onSpectrumError);
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: VisualizerPainter(_spectrum)
);
}
}
複製代碼
顯然用現有的組件咱們不太好拼出來頻譜的柱狀圖,因此須要本身來畫出來了。在Android中咱們會去自定一個View
而後重寫onDraw
來畫,在Flutter中用CustomPaint
也能達到一樣的效果。建立CustomPaint
的時候須要傳入一個painter
參數。具體在畫布上畫些什麼東西就是由這個painter
來決定的。因此咱們自定義了一個VisualizerPainter
來畫頻譜柱狀圖。
class VisualizerPainter extends CustomPainter {
// 頻譜數據
final Uint8List _spectrum;
VisualizerPainter(this._spectrum);
@override
void paint(Canvas canvas, Size size) {
// 先畫個黑色的背景
var rect = Offset.zero & size;
canvas.drawRect(
rect,
Paint()..color = Color(0xFF000000)
);
// 給個好看的顏色
LinearGradient gradient = LinearGradient(colors: [const Color(0xFF33FF33), const Color(0xFFFF0033)], begin: Alignment.bottomCenter, end: Alignment.topCenter);
// 每一個柱子的寬度
double columnWidth = size.width / COLUMNS_COUNT;
// 幅度比例
double step = size.height / 127;
// 挨個畫頻譜柱子
for (int i=0; i<COLUMNS_COUNT; i++) {
double volume = 2.0;
if (_spectrum != null && i < _spectrum.length) {
volume = _spectrum[i] * step + 2;
}
Rect column = Rect.fromLTRB(columnWidth*i, size.height-volume, columnWidth*i+columnWidth - 1, size.height);
canvas.drawRect(
column,
Paint()..shader = gradient.createShader(column)
);
}
}
@override
// 只有在頻譜數據發生變化的時候才重繪
bool shouldRepaint(VisualizerPainter oldDelegate) =>oldDelegate._spectrum != _spectrum;
}
複製代碼
當有新的頻譜數據傳過來的時候,調用setState
觸發重繪
void _onSpectrum(Object event) {
setState(() {
_spectrum = event;
});
}
複製代碼
最後在main.dart
裏把Visualizer
加上就好了。來看看效果
給頻譜柱子加個回落的動畫須要知道每次UI刷新的信號,也就是vsync信號,若是刷新率是60fps的話大概就是16ms一個vsync信號。Flutter中的Ticker
能夠提供這個vsync信號。Tiker
啓動之後會在每次vsync信號到來的時候回調你設置的callback。原本咱們能夠直接使用Tiker
,可是直接使用的話管理起來比較麻煩。還好Flutter有個AnimationController
,AnimationController
內部包含了一個Tiker
,而且提供了其餘的一些控制邏輯。用起來比較方便。
// 用SingleTickerProviderStateMixin擴展VisualizerState
class VisualizerState extends State<Visualizer> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
// 建立個AnimationController 時長200ms。
_controller = AnimationController(duration: Duration(milliseconds: 200), vsync: this);
// 設個callabck
_controller.addListener(_onTick);
}
複製代碼
建立AnimationController
的時候須要傳入vsync
參數。這須要State自身擴展SingleTickerProviderStateMixin
。而後把本身傳進去就行了。200ms的時長是應爲頻譜數據基本上會每隔100ms從Native傳過來一波。200ms的話保障動畫會在下次新頻譜數據過來以前會持續播放,而且在音頻中止之後不會一直無效的調用回調。在_onTick
回調裏面把每一個頻譜幅度減1製造回落的效果,調用setState
觸發Visualizer
重繪。
void _onTick() {
setState(() {
for (int i=0; i<COLUMNS_COUNT; i++) {
_spectrum[i] = (_spectrum[i] - 1).clamp(0, 127);
}
});
}
複製代碼
最後從新熱重載一下,而且打開Performance Overlay看一下性能。具體性能檢測工具怎麼用能夠去看官方文檔。
本文主要介紹瞭如何使用Flutter的CustomPainter
本身繪製音樂柱狀頻譜圖。固然你也能夠用CustomPainter
來繪製任何其餘圖形(好比各類圖表)。而後咱們又用AnimationController
來美化了一下頻譜圖,讓頻譜的表現更加平滑天然。最後咱們使用Flutter提供的性能檢測工具Performance Overlay觀察了一下Flutter app的性能。總的感想就有兩點:
那麼,你還在等什麼,趕快投身Flutter開發吧。