关于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开发吧。