App高级感营造之 动态按钮

161 阅读1分钟

效果

GIF 2023-5-31 9-41-55.gif

源代码

组件代码 animator_button.dart

import 'package:flutter/material.dart';

class MyController extends ChangeNotifier {
  DownloadState _downloadState = DownloadState.idle;

  resetState() {
    _downloadState = DownloadState.idle;
    notifyListeners();
  }
}

class AnimatorButtonWidget extends StatefulWidget {
  final Color mainColor;

  final MyController controller;

  final double btnWidth;

  const AnimatorButtonWidget({
    super.key,
    required this.mainColor,
    required this.btnWidth,
    required this.controller,
  });

  @override
  State<StatefulWidget> createState() {
    return AnimatorButtonWidgetState();
  }
}

enum DownloadState { idle, downloading, completed }

class AnimatorButtonWidgetState extends State<AnimatorButtonWidget>
    with TickerProviderStateMixin {
  /// 下载进度控制器
  late AnimationController _progressController;

  /// 缩放控制, 点击按钮时进行短暂缩放
  late AnimationController _scaleController;

  /// 下载完成之后 下载按钮的消失动画
  late AnimationController _fadeController;

  late Animation _scaleAnimation;
  late Animation _progressAnimation;
  late Animation _fadeAnimation;

  late final double _btnWidth;

  /// 遮罩层透明度
  double _barColorOpacity = .4;

  /// 按钮状态
  DownloadState _downloadState = DownloadState.idle;

  final TextStyle _textStyle =
      const TextStyle(color: Colors.white, fontSize: 18);

  /// 当前的下载进度
  int _downloadProgressValue = 0;

  @override
  void initState() {
    super.initState();

    widget.controller.addListener(() {
      if (widget.controller._downloadState == DownloadState.idle) {
        resetStatue();
      }
    });

    _btnWidth = widget.btnWidth;
    _progressController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 3),
    );

    _scaleController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 200),
    );

    _fadeController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    _fadeAnimation = Tween<double>(begin: 50, end: 0).animate(_fadeController)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          _scaleController.reverse();
          _fadeController.forward();
          _progressController.forward();
        }
      });

    _scaleAnimation =
        Tween<double>(begin: 1, end: 1.05).animate(_scaleController)
          ..addStatusListener((status) async {
            if (status == AnimationStatus.completed) {
              _scaleController.reverse();
              _fadeController.forward();

              _progressController.reset();
              _progressController.forward();
            }
          });

    _progressAnimation =
        Tween<double>(begin: 0, end: _btnWidth).animate(_progressController)
          ..addListener(() {
            setState(() {
              _downloadProgressValue =
                  ((100 * _progressAnimation.value / _btnWidth)).floor();
            });
          })
          ..addStatusListener((status) {
            debugPrint('进度状态发生变化');
            if (status == AnimationStatus.forward) {
              setState(() {
                _downloadState = DownloadState.downloading;
                _barColorOpacity = 0.4;
              });
            } else if (status == AnimationStatus.completed) {
              setState(() {
                _downloadState = DownloadState.completed;
                _barColorOpacity = 0;
              });
            }
          });
  }

  @override
  void dispose() {
    super.dispose();
    _progressController.dispose();
    _scaleController.dispose();
    _fadeController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        debugPrint('当前状态是 $_downloadState');
        if (_downloadState == DownloadState.idle) {
          startDownload();
        } else if (_downloadState == DownloadState.completed) {
          resetStatue();
        } else {
          debugPrint('下载中,点了没用 $_downloadProgressValue');
        }
      },
      child: Stack(children: [
        _mainLayout(),
        _loadingLayout(),
      ]),
    );
  }

  void startDownload() {
    _scaleController.forward();
  }

  void resetStatue() {
    setState(() {
      _downloadState = DownloadState.idle;
    });
    _fadeController.reverse();
    _progressController.reset();
  }

  void setDownloading() {
    setState(() {
      _downloadState = DownloadState.downloading;
    });
  }

  /// 主要布局
  Widget _mainLayout() {
    return AnimatedBuilder(
        animation: _scaleController,
        builder: (BuildContext context, Widget? child) {
          return Transform.scale(
              scale: _scaleAnimation.value,
              child: ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Container(
                      width: _btnWidth,
                      height: 50,
                      color: widget.mainColor.withOpacity(.7),
                      child: Row(children: [
                        _textActionButton(),
                        _downloadActionBtn()
                      ]))));
        });
  }

  /// loading遮罩层
  Widget _loadingLayout() {
    return AnimatedBuilder(
        animation: _progressController,
        builder: (BuildContext context, Widget? child) {
          return Positioned(
              left: 0,
              top: 0,
              height: 50,
              width: _progressAnimation.value,
              child: AnimatedOpacity(
                  opacity: _barColorOpacity,
                  duration: const Duration(milliseconds: 200),
                  child: ClipRRect(
                      borderRadius: BorderRadius.circular(10),
                      child: Container(color: Colors.white))));
        });
  }

  /// 文字部分
  Widget _textActionButton() {
    Widget w;
    switch (_downloadState) {
      case DownloadState.idle:
        w = Text('开始下载', style: _textStyle);
        break;
      case DownloadState.downloading:
        w = Text('$_downloadProgressValue%', style: _textStyle);
        break;
      case DownloadState.completed:
        w = Text('已完成', style: _textStyle);
        break;
    }

    return Expanded(child: Center(child: w));
  }

  /// 下载箭头按钮
  Widget _downloadActionBtn() {
    var decoration = const BorderRadius.only(
        topLeft: Radius.circular(0),
        bottomLeft: Radius.circular(0),
        topRight: Radius.circular(10),
        bottomRight: Radius.circular(10));

    return AnimatedBuilder(
        builder: (BuildContext context, Widget? child) {
          return Container(
              decoration: BoxDecoration(
                  color: widget.mainColor, borderRadius: decoration),
              width: _fadeAnimation.value,
              height: 50,
              child: const Icon(Icons.arrow_downward, color: Colors.white));
        },
        animation: _fadeController);
  }
}

main.dart代码:

import 'package:flutter/material.dart';
import 'package:flutter_neumorphism/widget/animator_button.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '动态按钮',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final MyController _myController = MyController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.grey[300],
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  _myController.resetState();
                },
                child: const Text('重置状态'),
              ),
              const SizedBox(height: 20),
              AnimatorButtonWidget(
                mainColor: Colors.blue,
                btnWidth: 200,
                controller: _myController,
              ),
              const SizedBox(height: 20),
              AnimatorButtonWidget(
                mainColor: Colors.orange,
                btnWidth: 200,
                controller: _myController,
              ),
              const SizedBox(height: 20),
              AnimatorButtonWidget(
                mainColor: Colors.black87,
                btnWidth: 300,
                controller: _myController,
              ),
              const SizedBox(height: 20),
              AnimatorButtonWidget(
                mainColor: Colors.deepPurple,
                btnWidth: 300,
                controller: _myController,
              ),
            ],
          ),
        ));
  }
}

实现原理

这是Flutter封装组件的常见写法,自定义StatefulWidget。

  1. 给一个widget设计多种状态(idle, downloading, completed)分别对应初始状态,下载中,已完成。
  2. 每一种状态对应了一种静态布局
/// 文字部分
Widget _textActionButton() {
  Widget w;
  switch (_downloadState) {
    case DownloadState.idle:
      w = Text('开始下载', style: _textStyle);
      break;
    case DownloadState.downloading:
      w = Text('$_downloadProgressValue%', style: _textStyle);
      break;
    case DownloadState.completed:
      w = Text('已完成', style: _textStyle);
      break;
  }

  return Expanded(child: Center(child: w));
}
  1. 结合 动画组件 AnimatedBuilder ,动画 Animation,以及 动画控制器 AnimationController 构建动态效果,多个动画组件 结合 Widget的自身状态, 创造出 widget状态发生变化时的动态效果。

  2. 最后开放一个外部控制器,可以由外部代码控制Widget的自身状态。