写在前头
本来是要做一个仿网易云音乐的flutter项目,但是因为最近事情比较多,项目周期跨度会比较长,因此分几个步骤来完成。这是仿网易云音乐项目系列文章的第一篇。没有完全照搬网易云音乐的UI,借鉴了其中的黑胶唱机动画。
先贴上项目地址 https://github.com/KinsomyJS/flutter_netease_music
初步效果图
思路
这个界面实现起来其实是比较简单的,大致分为如下几个部分:
- 1.背景的高斯模糊效果
- 2.黑胶唱头的旋转动画
- 3.黑胶唱片的旋转动画
- 4.下部控制器和进度条部分
我们一个个来说实现过程。
实践
整个界面是一个堆叠视图,最下面是一个背景图片,上面覆盖一层高斯模糊半透明遮罩,再上层是title,黑胶唱机和控制器。
1. 背景高斯模糊
首先使用stack组件用来包裹堆叠视图,在里面有两个container,第一个是背景网络图片,第二个就是一个BackdropFilter
。
Stack(
children: <Widget>[
new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new NetworkImage(coverArt),
fit: BoxFit.cover,
colorFilter: new ColorFilter.mode(
Colors.black54,
BlendMode.overlay,
),
),
),
),
new Container(
child: new BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Opacity(
opacity: 0.6,
child: new Container(
decoration: new BoxDecoration(
color: Colors.grey.shade900,
),
),
),
)),
...
]
这里的高斯模糊sigmaX和sigmaY的值选择了10,然后透明度为0.6,颜色为grey.shade900。
2.黑胶唱头的旋转动画
关于动画的知识这里就不做详细介绍了,可以参考官方文档传送门
自定义动画组件在needle_anim.dart
文件里。
这里将动画和组件解耦,分别定义了动画过程类PivotTransition
,顾名思义围绕一个支点旋转,继承自AnimatedWidget
。
支点定在child组件的topcenter位置。 注意turns不能为空,需要根据turns的值计算旋转绕过的周长,围绕Z轴旋转。
class PivotTransition extends AnimatedWidget {
/// 创建旋转变换
/// turns不能为空.
PivotTransition({
Key key,
this.alignment: FractionalOffset.topCenter,
@required Animation<double> turns,
this.child,
}) : super(key: key, listenable: turns);
/// The animation that controls the rotation of the child.
/// If the current value of the turns animation is v, the child will be
/// rotated v * 2 * pi radians before being painted.
Animation<double> get turns => listenable;
/// The pivot point to rotate around.
final FractionalOffset alignment;
/// The widget below this widget in the tree.
final Widget child;
@override
Widget build(BuildContext context) {
final double turnsValue = turns.value;
final Matrix4 transform = new Matrix4.rotationZ(turnsValue * pi * 2.0);
return new Transform(
transform: transform,
alignment: alignment,
child: child,
);
}
}
接下来就是自定义黑胶唱头组件。
final _rotateTween = new Tween<double>(begin: -0.15, end: 0.0);
new Container(
child: new PivotTransition(
turns: _rotateTween.animate(controller_needle),
alignment: FractionalOffset.topLeft,
child: new Container(
width: 100.0,
child: new Image.asset("images/play_needle.png"),
),
),
),
将png图片包裹在container内作为child参数传递给PivotTransition
。
外部使用的时候传入一个Tween,起始位置为-0.15 ~ 0.0。
3.黑胶唱片的旋转动画
这部分代码在record_anim.dart
文件内。使用了package:flutter/animation.dart
提供的RotationTransition
做旋转,很简单。
class RotateRecord extends AnimatedWidget {
RotateRecord({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: 250.0,
width: 250.0,
child: new RotationTransition(
turns: animation,
child: new Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(
"https://images-na.ssl-images-amazon.com/images/I/51inO4DBH0L._SS500.jpg"),
),
),
)),
);
}
}
接着自定义旋转动画的控制逻辑。旋转一圈用时十五秒钟,速度为线性匀速,同时会重复旋转动画。
controller_record = new AnimationController(
duration: const Duration(milliseconds: 15000), vsync: this);
animation_record =
new CurvedAnimation(parent: controller_record, curve: Curves.linear);
animation_record.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller_record.repeat();
} else if (status == AnimationStatus.dismissed) {
controller_record.forward();
}
});
4.下部控制器和进度条部分
播放流媒体音频使用了三方组件audioplayers
,具体代码在player_page.dart
文件内,封装了一个player组件,接受了一系列参数包括音频路径,播放操作回调等。该组件支持本地资源和网络资源,这里用网络音频资源做demo。
const Player(
{@required this.audioUrl,
@required this.onCompleted,
@required this.onError,
@required this.onNext,
@required this.onPrevious,
this.key,
this.volume: 1.0,
this.onPlaying,
this.color: Colors.white,
this.isLocal: false});
在initState方法里初始化AudioPlayer
对象。”..”是dart的级联操作符。
audioPlayer = new AudioPlayer();
audioPlayer
..completionHandler = widget.onCompleted
..errorHandler = widget.onError
..durationHandler = ((duration) {
setState(() {
this.duration = duration;
if (position != null) {
this.sliderValue = (position.inSeconds / duration.inSeconds);
}
});
})
..positionHandler = ((position) {
setState(() {
this.position = position;
if (duration != null) {
this.sliderValue = (position.inSeconds / duration.inSeconds);
}
});
});
开始播放代码
audioPlayer.play(
widget.audioUrl,
isLocal: widget.isLocal,
volume: widget.volume,
);
开始播放后,durationHandler
会回调音频总时长,positionHandler
会回调播放进度,两个回调都返回一个Duration
对象。根据这两个duration对象可以计算机播放进度的百分比,这里使用Slider
组件做进度条。
new Slider(
onChanged: (newValue) {
if (duration != null) {
int seconds = (duration.inSeconds * newValue).round();
print("audioPlayer.seek: $seconds");
audioPlayer.seek(new Duration(seconds: seconds));
}
},
value: sliderValue ?? 0.0,
activeColor: widget.color,
),
总结
整体实现是非常简单的,只要对flutter的组件有所了解就能很快写出来,后面还会加入歌词滚动功能来丰富界面。
具体项目可以到 https://github.com/KinsomyJS/flutter_netease_music 查看,也欢迎star持续关注。