Flutter仿網易雲音樂:播放介面

kinsomy發表於2019-01-08

寫在前頭

本來是要做一個仿網易雲音樂的flutter專案,但是因為最近事情比較多,專案週期跨度會比較長,因此分幾個步驟來完成。這是仿網易雲音樂專案系列文章的第一篇。沒有完全照搬網易雲音樂的UI,借鑑了其中的黑膠唱機動畫。

先貼上專案地址 github.com/KinsomyJS/f…

初步效果圖

Flutter仿網易雲音樂:播放介面

思路

這個介面實現起來其實是比較簡單的,大致分為如下幾個部分:

  • 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的元件有所瞭解就能很快寫出來,後面還會加入歌詞滾動功能來豐富介面。

具體專案可以到 github.com/KinsomyJS/f… 檢視,也歡迎star持續關注。

參考資料

  1. 官方文件
  2. pub: audioplayers

相關文章