前言
此项目复刻了网易云播放页中唱片转动的部分功能,效果如下:
具体实现
状态栏透明
@override
void initState() {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent,));
super.initState();
}
模糊背景
使用BackdropFilter实现模糊背景。先将专辑封面填满整个屏幕,然后添加模糊滤镜。代码中使用了两次BackdropFilter是因为唱片四周还有一圈模糊背景。
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 40.0, sigmaY: 40.0),
child: Container(
padding: EdgeInsets.only(top: _height/7),
alignment: Alignment.topCenter,
color: Colors.black.withOpacity(0.2),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Container(
child: TurnTable(playController: _playController,),
width: 300,
height: 300,
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.all(50),
decoration: BoxDecoration(shape: BoxShape.circle,
color: Colors.white.withOpacity(0.2),),
),
),
),
),
唱片实现
唱片的实现在TurnTable类。使用RotationTransition实现旋转动画。使用CustomPaint绘制唱片四周的黑边,暂时没有绘制复杂的纹理。TurnTable使用PlayController控制是否开始转动唱片。完整代码如下:
class TurnTable extends StatefulWidget {
const TurnTable({Key? key,required this.playController}) : super(key: key);
final PlayController playController;
@override
_TurnTableState createState() => _TurnTableState();
}
class _TurnTableState extends State<TurnTable>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final String _albumArtUrl = "https://img.1ting.com/images/special/194/2aaa90fb9d24eb005838d3b8b6ddca34.jpg";
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.duration = Duration(seconds: 3);
widget.playController.addListener(updatePlayStatus);
}
@override
void dispose() {
_controller.dispose();
widget.playController.removeListener(updatePlayStatus);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
child: RotationTransition(
turns: _controller,
child: CustomPaint(
painter: _TurnTablePainter(),
child: Padding(
padding: const EdgeInsets.all(50.0),
child: ClipOval(
child: Image.network(
_albumArtUrl,
fit: BoxFit.cover,
),
),
),
),
),
);
}
updatePlayStatus(){
setState(() {
if(widget.playController.isPlaying){
_controller.repeat();
}else {
_controller.stop();
}
});
}
}
class _TurnTablePainter extends CustomPainter{
static final Paint _turnTablePaint = Paint()
..color = Colors.black;
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(Offset(size.width/2,size.height/2), size.width/2, _turnTablePaint);
}
@override
bool shouldRepaint(covariant _TurnTablePainter oldDelegate) {
return false;
}
}
唱臂实现
唱臂的样式稍微有点复杂,所以没有使用CustomPaint绘制,而是直接使用图片资源来实现。同样使用RotationTransition实现转动功能。同样使用使用PlayController控制动画。代码如下:
class TurnTableArm extends StatefulWidget {
const TurnTableArm({Key? key,required this.playController}) : super(key: key);
final PlayController playController;
@override
State<TurnTableArm> createState() => _TurnTableArmState();
}
class _TurnTableArmState extends State<TurnTableArm> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.duration = Duration(seconds: 3);
widget.playController.addListener(updatePlayStatus);
}
@override
void dispose() {
_controller.dispose();
widget.playController.removeListener(updatePlayStatus);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RotationTransition(
turns: _controller,
child: Image.asset('assets/images/turntable_arm.png'),
),
),
);
}
updatePlayStatus(){
setState(() {
widget.playController.isPlaying ? _controller.animateTo(.1) : _controller.reverse();
});
}
}
PlayController
继承ChangeNotifier,保存播放状态,并通知界面改变。有优化空间。
class PlayController extends ChangeNotifier{
bool _isPlaying = false;
get isPlaying => _isPlaying;
play(){
_isPlaying = true;
notifyListeners();
}
stop(){
_isPlaying = false;
notifyListeners();
}
}
AnimatedPlayPauseButton
封装了一个播放控制按钮,代码如下:
import 'package:flutter/material.dart';
class AnimatedPlayPauseButton extends StatefulWidget {
const AnimatedPlayPauseButton({
Key? key,
required this.isPlaying,
required this.onTap,
this.color,
this.size})
: super(key: key);
final bool isPlaying;
final double? size;
final VoidCallback onTap;
final Color? color;
@override
State<AnimatedPlayPauseButton> createState() =>
_AnimatedPlayPauseButtonState();
}
class _AnimatedPlayPauseButtonState extends State<AnimatedPlayPauseButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
value: widget.isPlaying ? 1 : 0,
duration:const Duration(milliseconds: 300),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant AnimatedPlayPauseButton oldWidget) {
super.didUpdateWidget(oldWidget);
if(widget.isPlaying != oldWidget.isPlaying){
widget.isPlaying ? _controller.forward() : _controller.reverse();
}
}
@override
Widget build(BuildContext context) {
return Center(
child: InkWell(
onTap: widget.onTap,
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(width: 2,color: Colors.white),
shape: BoxShape.circle,
),
child: AnimatedIcon(
color: widget.color,
icon: AnimatedIcons.play_pause,
progress: _controller,
size: widget.size,
),
),
),
);
}
}