[Flutter 布局] 稍微复刻一下网易云的播放页

540 阅读2分钟

前言

此项目复刻了网易云播放页中唱片转动的部分功能,效果如下:

netease_music.gif

具体实现

状态栏透明

@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,
          ),
        ),
      ),
    );
  }
}