0

Flutter Animation : Creating medium’s clap animation in flutter

Trong bài viết này, tôi sẽ giới thiệu cho các bạn về animation trong Flutter. Chúng ta sẽ tìm hiểu một số khái niệm cốt lõi về animation bằng cách tạo mô phỏng animation vỗ tay trong Flutter.

1. Getting Started

Chúng ta sẽ bắt đầu tìm hiểu từ đoạn code có sẵn khi tạo một dự án mới. Let's go

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }
}

Đoạn mã trên xử lý việc đếm số lần chúng ta cick vào button trên màn hình. Sample này có một UI khá đơn giản, vì vậy tôi muốn nâng cấp nó lên bằng cách thêm các animation vào để có một UI bắt mắt hơn. Ví dụ như UI dưới đây

Hãy xem nhanh và khắc phục một số vấn đề dễ dàng trước khi thêm animation.

  1. Thay đổi biểu tượng và nền của nút.
  2. Nút sẽ tiếp tục tăng số lượng khi chúng ta giữ nút.
  3. Hãy thêm 2 bản sửa lỗi nhanh đó và bắt đầu với hoạt ảnh.
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  final duration = new Duration(milliseconds: 300);
  Timer timer;


  initState() {
    super.initState();
  }

  dispose() {
   super.dispose();
  }

  void increment(Timer t) {
    setState(() {
      _counter++;
    });
  }

  void onTapDown(TapDownDetails tap) {
    // User pressed the button. This can be a tap or a hold.
    increment(null); // Take care of tap
    timer = new Timer.periodic(duration, increment); // Takes care of hold
  }

  void onTapUp(TapUpDetails tap) {
    // User removed his finger from button.
    timer.cancel();
  }

  Widget getScoreButton() {

    return new Positioned(
        child: new Opacity(opacity: 1.0, child: new Container(
            height: 50.0 ,
            width: 50.0 ,
            decoration: new ShapeDecoration(
              shape: new CircleBorder(
                  side: BorderSide.none
              ),
              color: Colors.pink,
            ),
            child: new Center(child:
            new Text("+" + _counter.toString(),
              style: new TextStyle(color: Colors.white,
                  fontWeight: FontWeight.bold,
                  fontSize: 15.0),))
        )),
        bottom: 100.0
    );
  }

  Widget getClapButton() {
    // Using custom gesture detector because we want to keep increasing the claps
    // when user holds the button.
    return new GestureDetector(
        onTapUp: onTapUp,
        onTapDown: onTapDown,
        child: new Container(
          height: 60.0 ,
          width: 60.0 ,
          padding: new EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
              border: new Border.all(color: Colors.pink, width: 1.0),
              borderRadius: new BorderRadius.circular(50.0),
              color: Colors.white,
              boxShadow: [
                new BoxShadow(color: Colors.pink, blurRadius: 8.0)
              ]
          ),
          child: new ImageIcon(
              new AssetImage("images/clap.png"), color: Colors.pink,
              size: 40.0),
        )
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme
                  .of(context)
                  .textTheme
                  .display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new Padding(
          padding: new EdgeInsets.only(right: 20.0),
          child: new Stack(
            alignment: FractionalOffset.center,
            overflow: Overflow.visible,
            children: <Widget>[
              getScoreButton(),
              getClapButton(),
            ],
          )
      ),
    );
  }
}

Nhìn vào sản phẩm cuối cùng, có 3 điều chúng tôi cần bổ sung.

  1. Thay đổi kích thước của vật dụng.
  2. Hiển thị tiện ích điểm số khi nút được nhấn và ẩn nó khi được phát hành.
  3. Thêm những widget rắc nhỏ đó và tạo hiệu ứng cho chúng. Hãy thực hiện từng thứ một. Để bắt đầu, chúng ta cần hiểu một số điều cơ bản về animation trong Flutter.

2. Tìm hiểu các thành phần cho một animation cơ bản trong Flutter

Animaiton đơn giản là việc thay đổi một số giá trị theo thời gian. Ví dụ: khi chúng ta nhấp vào nút, chúng ta muốn tạo ra animation điểm số tăng từ dưới lên và khi chúng ta nhấn giữ nút, nó sẽ tăng lên nhiều hơn và sau đó ẩn đi. Nếu bạn chỉ nhìn vào điểm, chúng ta cần thay đổi các giá trị của vị trí và độ mờ của widget theo thời gian.

new Positioned(
        child: new Opacity(opacity: 1.0, 
          child: new Container(
            ...
          )),
        bottom: 100.0
    );

Giả sử chúng tôi muốn tiện ích con điểm số mất 150 mili giây để tự hiển thị từ phía dưới.

Đây là một đồ thị 2D đơn giản. Vị trí sẽ thay đổi theo thời gian. Chú ý rằng đường chéo là đường thẳng. Đây thậm chí có thể là một đường cong nếu bạn thích. Bạn có thể làm cho vị trí tăng từ từ theo thời gian và sau đó ngày càng nhanh hơn. Hoặc bạn có thể chạy với tốc độ siêu cao và sau đó giảm tốc độ ở cuối. Đây là thành phần đầu tiên mà tôi muốn giới thiệu: **Animation Controller. **

scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);

Ở đây, chúng tôi đã tạo một bộ điều khiển đơn giản cho hoạt ảnh. Chúng tôi đã chỉ định rằng chúng tôi muốn chạy hoạt ảnh trong khoảng thời gian 150ms. Nhưng, vsync đó là gì? Thiết bị di động làm mới màn hình của chúng sau mỗi vài mili giây. Đó là cách chúng ta cảm nhận tập hợp hình ảnh như một dòng chảy liên tục hoặc một bộ phim. Tốc độ làm mới màn hình có thể khác nhau giữa các thiết bị. Giả sử thiết bị di động làm mới màn hình 60 lần một giây (60 khung hình mỗi giây). Tức là cứ sau 16,67 mili giây, chúng ta đưa một hình ảnh mới vào não. Đôi khi, mọi thứ có thể bị lệch (chúng tôi gửi ra một hình ảnh khác trong khi màn hình đang làm mới) và chúng tôi nhận thấy hiện tượng rách màn hình. VSync sẽ giải quyết vấn đề đó.

scoreInAnimationController.addListener(() {
      print(scoreInAnimationController.value);
    });
scoreInAnimationController.forward(from: 0.0);

/* OUTPUT
I/flutter ( 1913): 0.0
I/flutter ( 1913): 0.0
I/flutter ( 1913): 0.22297333333333333
I/flutter ( 1913): 0.3344533333333333
I/flutter ( 1913): 0.4459333333333334
I/flutter ( 1913): 0.5574133333333334
I/flutter ( 1913): 0.6688933333333335
I/flutter ( 1913): 0.7803666666666668
I/flutter ( 1913): 0.8918466666666668
I/flutter ( 1913): 1.0
*/

Controller ra các số từ 0,0 đến 1,0 trong 150 ms. Lưu ý rằng các giá trị được tạo ra gần như tuyến tính. 0,2, 0,3, 0,4… Chúng ta thay đổi hành vi này như thế nào? Điều này sẽ được thực hiện bởi thành phần thứ hai: Curved Animation

bounceInAnimation = new CurvedAnimation(parent: scoreInAnimationController, curve: Curves.bounceIn);
    bounceInAnimation.addListener(() {
      print(bounceInAnimation.value);
    });

/*OUTPUT
I/flutter ( 5221): 0.0
I/flutter ( 5221): 0.0
I/flutter ( 5221): 0.24945376519722218
I/flutter ( 5221): 0.16975716286388898
I/flutter ( 5221): 0.17177866222222238
I/flutter ( 5221): 0.6359024059750003
I/flutter ( 5221): 0.9119433941222221
I/flutter ( 5221): 1.0
*/

CurveAnimation cung cấp đường cong mà chúng tôi muốn theo dõi. Có một loạt các lựa chọn về đường cong mà chúng ta có thể sử dụng tại trang tài liệu về Curved Animation trong Flutter. Controller cung cấp giá trị từ 0,0 đến 1,0 cho tiện ích hoạt ảnh cong trong khoảng thời gian 150 ms. Widget hoạt ảnh cong sẽ nội suy các giá trị đó theo đường cong mà chúng tôi đã đặt. Tuy nhiên, chúng tôi đang nhận được giá trị từ 0,0 đến 1,0. Nhưng chúng tôi muốn các giá trị từ 0,0 đến 100,0 cho tiện ích con điểm số của chúng tôi. Chúng tôi chỉ cần nhân với 100 để có kết quả. Hoặc chúng ta có thể sử dụng thành phần thứ ba: The Tween Class

tweenAnimation = new Tween(begin: 0.0, end: 100.0).animate(scoreInAnimationController);
    tweenAnimation.addListener(() {
      print(tweenAnimation.value);
    });

/* Output 
I/flutter ( 2639): 0.0
I/flutter ( 2639): 0.0
I/flutter ( 2639): 33.452000000000005
I/flutter ( 2639): 44.602000000000004
I/flutter ( 2639): 55.75133333333334
I/flutter ( 2639): 66.90133333333334
I/flutter ( 2639): 78.05133333333333
I/flutter ( 2639): 89.20066666666668
I/flutter ( 2639): 100.0
*/

Lớp tween tạo ra các giá trị từ đầu đến cuối. Chúng tôi đã sử dụng scoreInAnimationController trước đó, sử dụng một đường cong tuyến tính. Thay vào đó, chúng tôi có thể sử dụng đường bounce curve để nhận được giá trị khác. Ưu điểm của Tween không kết thúc ở đây.

3. Thay đổi vị trí Score Widget với Position Animation

Tại thời điểm này, chúng ta đã có đủ kiến thức để làm cho tiện ích điểm số của chúng ta chuyển ra từ phía dưới khi chúng ta nhấn nút và ẩn khi chúng ta bỏ nhấn nút.

initState() {
    super.initState();
    scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
    scoreInAnimationController.addListener((){
      setState(() {}); // Calls render function
    });
  }

void onTapDown(TapDownDetails tap) {
    scoreInAnimationController.forward(from: 0.0);
    ...    
}
Widget getScoreButton() {
    var scorePosition = scoreInAnimationController.value * 100;
    var scoreOpacity = scoreInAnimationController.value;
    return new Positioned(
        child: new Opacity(opacity: scoreOpacity, 
                           child: new Container(...)
                          ),
        bottom: scorePosition
    );
  }

Tiện ích điểm số sẽ chạy ra khi chạm vào. Nhưng vẫn còn một vấn đề. Khi chúng ta nhấn vào nút nhiều lần, tiện ích điểm số sẽ bật ra nhiều lần. Điều này là do một lỗi nhỏ trong đoạn mã trên. Chúng tôi đang yêu cầu bộ điều khiển chuyển tiếp từ 0 mỗi khi nhấn vào một nút. Bây giờ, hãy thêm hoạt ảnh cho tiện ích điểm số. Đầu tiên, chúng tôi thêm một enum để quản lý trạng thái của tiện ích con điểm dễ dàng hơn.

enum ScoreWidgetStatus {
  HIDDEN,
  BECOMING_VISIBLE,
  BECOMING_INVISIBLE
}

Sau đó, chúng tôi tạo ra một bộ điều khiển hoạt ảnh. Bộ điều khiển hoạt ảnh sẽ tạo hoạt ảnh cho vị trí của tiện ích từ 100 đến 150 một cách không tuyến tính. Chúng tôi cũng đã thêm một trình nghe trạng thái cho hoạt ảnh. Ngay sau khi hoạt ảnh kết thúc, chúng tôi đặt trạng thái của tiện ích điểm số của chúng tôi thành ẩn.

scoreOutAnimationController = new AnimationController(vsync: this, duration: duration);
    scoreOutPositionAnimation = new Tween(begin: 100.0, end: 150.0).animate(
      new CurvedAnimation(parent: scoreOutAnimationController, curve: Curves.easeOut)
    );
    scoreOutPositionAnimation.addListener((){
      setState(() {});
    });
    scoreOutAnimationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _scoreWidgetStatus = ScoreWidgetStatus.HIDDEN;
      }
    });

Khi người dùng loại bỏ ngón tay của mình khỏi tiện ích, chúng tôi sẽ đặt trạng thái cho phù hợp và khởi động bộ đếm thời gian 300 mili giây. Sau 300 mili giây, chúng tôi sẽ tạo hiệu ứng cho vị trí và độ mờ của tiện ích.

void onTapUp(TapUpDetails tap) {
    // User removed his finger from button.
    scoreOutETA = new Timer(duration, () {
      scoreOutAnimationController.forward(from: 0.0);
      _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_INVISIBLE;
    });
    holdTimer.cancel();
  }

Chúng tôi cũng đã sửa đổi sự kiện nhấn xuống để xử lý một số tình huống ở góc.

 void onTapDown(TapDownDetails tap) {
    // User pressed the button. This can be a tap or a hold.
    if (scoreOutETA != null) scoreOutETA.cancel(); // We do not want the score to vanish!
    if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN) {
      scoreInAnimationController.forward(from: 0.0);
      _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;
    }
    increment(null); // Take care of tap
    holdTimer = new Timer.periodic(duration, increment); // Takes care of hold
  }

Cuối cùng, chúng tôi cần chọn giá trị của bộ điều khiển mà chúng tôi cần sử dụng cho vị trí và độ mờ của tiện ích con điểm của chúng tôi. Một công tắc đơn giản thực hiện công việc.

Widget getScoreButton() {
    var scorePosition = 0.0;
    var scoreOpacity = 0.0;
    switch(_scoreWidgetStatus) {
      case ScoreWidgetStatus.HIDDEN:
        break;
      case ScoreWidgetStatus.BECOMING_VISIBLE :
        scorePosition = scoreInAnimationController.value * 100;
        scoreOpacity = scoreInAnimationController.value;
        break;
      case ScoreWidgetStatus.BECOMING_INVISIBLE:
        scorePosition = scoreOutPositionAnimation.value;
        scoreOpacity = 1.0 - scoreOutAnimationController.value;
    }
  return ...
}

Và đó là phần giới thiệu của chúng tôi về các hình ảnh động cơ bản trong Flutter. Tôi sẽ tiếp tục khám phá Flutter nhiều hơn và học cách tạo giao diện người dùng nâng cao.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí