写在前头
flutter 自定义实现音乐播放的文章挺多的,但是在开发中还是碰见了超级无语的情况, 没想到需求竟然要音频的1倍到2倍的播放倍速, 我一度质疑这个功能的实际用途,但是既然提出来了, 开发就得撅屁股实现,这里采用了第三方的视频播放器来实现倍速效果,具体如下
一、实现效果

二、使用步骤
1.引入库
代码如下(示例):
just_audio: ^0.8.0
flutter_seekbar:
git: https:
2.关键代码
1. 初始化播放器关键代码如下(示例):
class _audioplayerState extends State<audioplayer> {
AudioPlayer audioPlayer = AudioPlayer();
Stream<PositionData> get _positionDataStream =>
Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
audioPlayer.positionStream,
audioPlayer.bufferedPositionStream,
audioPlayer.durationStream,
(position, bufferedPosition, duration) =>
PositionData(
position, bufferedPosition, duration ?? Duration.zero));
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());
audioPlayer.playbackEventStream.listen((event) {},
onError: (Object e, StackTrace stackTrace) {
print('发生流错误: $e');
});
try {
await audioPlayer.setAudioSource(AudioSource.uri(Uri.parse(
"https://demo.dj63.com//2019/user_up/dj1523281909/20201103/01363f2c17b7119f023aa17c20f1be5f.mp3")));
} catch (e) {
print("加载音频源时出错: $e");
}
}
@override
void dispose() {
audioPlayer.dispose();
super.dispose();
}
}
2. 倍速进度条关键代码如下(示例):
Expanded(
child: Container(
padding: EdgeInsets.fromLTRB(10, 6, 0, 6),
width: 60,
child: SeekBar(
progresseight: 2,
min: 0,
max: 2,
value: AudioPlayerinfo.speed_progress,
sectionCount: 4,
indicatorRadius: 8.0,
backgroundColor: Colors.white,
progressColor: Colors.white,
onValueChanged: (v) async {
AudioPlayerinfo.setspeed_progress(v.value);
await audioPlayer.setSpeed(v.value+1);
},
)
),
)
3. 音频播放器的进度条关键代码如下,这里采用自定义的seekbar, 方便赋值状态(示例):
Scaffold(
backgroundColor:Color(0x00000000),
body: StreamBuilder<PositionData>(
stream: positionDataStream,
builder: (context, snapshot) {
final positionData = snapshot.data;
AudioPlayerinfo.setprogress(positionData?.position.inSeconds ?? 0);
return SeekBarlayout(
duration: positionData?.duration ?? Duration.zero,
position: positionData?.position ?? Duration.zero,
bufferedPosition:
positionData?.bufferedPosition ?? Duration.zero,
onChangeEnd: audioPlayer.seek,
);
},
),
),
4. 自定义进度条SeekBarlayout 代码(示例):
import 'dart:math';
import 'package:flutter/material.dart';
class SeekBarlayout extends StatefulWidget {
final Duration duration;
final Duration position;
final Duration bufferedPosition;
final ValueChanged<Duration>? onChanged;
final ValueChanged<Duration>? onChangeEnd;
SeekBarlayout({
required this.duration,
required this.position,
required this.bufferedPosition,
this.onChanged,
this.onChangeEnd,
});
@override
_SeekBarState createState() => _SeekBarState();
}
class _SeekBarState extends State<SeekBarlayout> {
double? _dragValue;
late SliderThemeData _sliderThemeData;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_sliderThemeData = SliderTheme.of(context).copyWith(
trackHeight: 2.0,
overlayColor: Colors.white,
thumbColor:Colors.white,
);
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 0),
padding: EdgeInsets.fromLTRB(0, 0, 0, 0),
child: Stack(
children: [
SliderTheme(
data: _sliderThemeData.copyWith(
thumbShape: HiddenThumbComponentShape(),
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white,
),
child: ExcludeSemantics(
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(widget.bufferedPosition.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
),
SliderTheme(
data: _sliderThemeData.copyWith(
inactiveTrackColor: Colors.transparent,
),
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
Positioned(
right: 16.0,
bottom: 0.0,
child: Text(
RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
.firstMatch("$duration")
?.group(1) ??
'$duration',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white,
decoration: TextDecoration.none,
),),
),
Positioned(
left: 16.0,
bottom: 0.0,
child: Text(RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
.firstMatch("$position")
?.group(1) ??
'$position',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white,
decoration: TextDecoration.none,
),),
),
],
),
);
}
Duration get _remaining => widget.duration - widget.position;
Duration get duration => widget.duration;
Duration get position => widget.position;
}
class HiddenThumbComponentShape extends SliderComponentShape {
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {}
}
class PositionData {
final Duration position;
final Duration bufferedPosition;
final Duration duration;
PositionData(this.position, this.bufferedPosition, this.duration);
}
void showSliderDialog({
required BuildContext context,
required String title,
required int divisions,
required double min,
required double max,
String valueSuffix = '',
required Stream<double> stream,
required ValueChanged<double> onChanged,
}) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(title, textAlign: TextAlign.center),
content: StreamBuilder<double>(
stream: stream,
builder: (context, snapshot) => Container(
height: 60,
width: 100,
child: Column(
children: [
Text('${snapshot.data?.toStringAsFixed(1)}$valueSuffix',
style: TextStyle(
fontFamily: 'Fixed',
fontWeight: FontWeight.bold,
fontSize: 24.0)),
Slider(
divisions: divisions,
min: min,
max: max,
value: snapshot.data ?? 1.0,
onChanged: onChanged,
),
],
),
),
),
),
);
}
5. 播放器暂停与播放, 以及快进5秒,回退5秒的关键代码(示例):
GestureDetector(
child: Image.asset("assets/tui5.png",
width: 40,
height: 38,
fit: BoxFit.cover,
colorBlendMode: BlendMode.darken,
),onTap: () async {
if(AudioPlayerinfo.progress > 5){
await audioPlayer.seek( Duration(seconds: AudioPlayerinfo.progress-5) );
}
if(!AudioPlayerinfo.isPlaying){
audioPlayer.play();
AudioPlayerinfo.set_isPlaying(!AudioPlayerinfo.isPlaying);
}
},
),Container(
margin: EdgeInsets.only(left: 40, right: 40),
child: GestureDetector(
onTap: () {
AudioPlayerinfo.set_isPlaying(!AudioPlayerinfo.isPlaying);
if(AudioPlayerinfo.isPlaying){
audioPlayer.stop();
}else{
audioPlayer.play();
}
},
child: Image.asset(AudioPlayerinfo.isPlaying
? "assets/stop.png"
: "assets/player.png",
width: 54,
height: 54,
fit: BoxFit.cover,
colorBlendMode: BlendMode.darken,
),
),
), GestureDetector(
child: Image.asset("assets/jin5.png",
width: 40,
height: 38,
fit: BoxFit.cover,
colorBlendMode: BlendMode.darken,
),onTap: () async {
await audioPlayer.seek( Duration(seconds: AudioPlayerinfo.progress+5) );
if(!AudioPlayerinfo.isPlaying){
audioPlayer.play();
AudioPlayerinfo.set_isPlaying(!AudioPlayerinfo.isPlaying);
}
},
),
总结
整体实现是非常简单的,只要对flutter的组件有所了解就能很快写出来,更多功能参数可以参考资料https://pub.flutter-io.cn/packages/just_audio,希望能够帮助到您。。。
|