🌺

【Dart/Flutter】スクロール時FloatingActionButton.extended⇔FloatingActionButton

2023/02/24に公開

パッケージ「flutter_scrolling_fab_animated」をシンプルにFloatingActionButton.extended⇔FloatingActionButtonと考えるアイデアとなります。

【Dart/Flutter】スクロール時FloatingActionButton.extended⇔FloatingActionButton

実行結果

コード

DartPadに貼り付け確認可能

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true),
      home: const Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  List<String> items = [for (int i = 0; i < 30; i++) i.toString()];
  final ScrollController _scrollController = ScrollController();
  double indicator = 10.0;
  bool onTop = true;

  
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Scrolling Fab Animated Demo'),
      ),
      body: ListView.builder(
          controller: _scrollController,
          itemCount: items.length,
          itemBuilder: (BuildContext ctxt, int index) {
            return Card(
                child: ListTile(
              title: Text(items[index]),
            ));
          }),
      floatingActionButton: ScrollingFabAnimated(
        icon: const Icon(
          Icons.add,
          color: Colors.white,
        ),
        text: const Text(
          'Add',
          style: TextStyle(color: Colors.white, fontSize: 16.0),
        ),
        onPress: () {
          print('OnPress!!');
        },
        scrollController: _scrollController,
        // animateIcon: true,
        // inverted: false,
      ),
    );
  }
}

/// Widget to animate the button when scroll down
class ScrollingFabAnimated extends StatefulWidget {
  /// Function to use when press the button
  final GestureTapCallback? onPress;

  /// Double value to set the button elevation
  final double? elevation;

  /// Double value to set the button width
  final double? width;

  /// Double value to set the button height
  final double? height;

  /// Value to set the duration for animation
  final Duration? duration;

  /// Widget to use as button icon
  final Widget? icon;

  /// Widget to use as button text when button is expanded
  final Widget? text;

  /// Value to set the curve for animation
  final Curve? curve;

  /// ScrollController to use to determine when user is on top or not
  final ScrollController? scrollController;

  /// Double value to set the boundary value when scroll animation is triggered
  final double? limitIndicator;

  /// Color to set the button background color
  final Color? color;

  /// Value to indicate if animate or not the icon
  final bool? animateIcon;

  /// Value to inverte the behavior of the animation
  final bool? inverted;

  /// Double value to set the button radius
  final double? radius;

  const ScrollingFabAnimated(
      {Key? key,
      required this.icon,
      required this.text,
      required this.onPress,
      required this.scrollController,
      this.elevation = 5.0,
      this.width = 120.0,
      this.height = 60.0,
      this.duration = const Duration(milliseconds: 250),
      this.curve,
      this.limitIndicator = 10.0,
      this.color,
      this.animateIcon = true,
      this.inverted = false,
      this.radius})
      : super(key: key);

  
  _ScrollingFabAnimatedState createState() => _ScrollingFabAnimatedState();
}

class _ScrollingFabAnimatedState extends State<ScrollingFabAnimated> {
  /// Double value for tween ending
  double _endTween = 100;

  
  void setState(VoidCallback fn) {
    if (mounted) super.setState(fn);
  }

  
  void initState() {
    super.initState();
    if (widget.inverted!) {
      setState(() {
        _endTween = 0;
      });
    }
    _handleScroll();
  }

  
  void dispose() {
    widget.scrollController!.removeListener(() {});
    super.dispose();
  }

  /// Function to add listener for scroll
  void _handleScroll() {
    ScrollController scrollController = widget.scrollController!;
    scrollController.addListener(() {
      if (scrollController.position.pixels > widget.limitIndicator! &&
          scrollController.position.userScrollDirection ==
              ScrollDirection.reverse) {
        setState(() {
          _endTween = widget.inverted! ? 100 : 0;
        });
      } else if (scrollController.position.pixels <= widget.limitIndicator! &&
          scrollController.position.userScrollDirection ==
              ScrollDirection.forward) {
        setState(() {
          _endTween = widget.inverted! ? 0 : 100;
        });
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Card(
      elevation: widget.elevation,
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.all(Radius.circular(widget.height! / 2))),
      child: TweenAnimationBuilder(
        tween: Tween<double>(begin: 0, end: _endTween),
        duration: widget.duration!,
        builder: (BuildContext _, double size, Widget? child) {
          double widthPercent = (widget.width! - widget.height!).abs() / 100;
          bool isFull = _endTween == 100;
          return SizedBox(
            height: widget.height,
            width: widget.height! + widthPercent * size,
            child: Transform.rotate(
              angle: widget.animateIcon! ? (3.6 * math.pi / 180) * size : 0,
              child: isFull
                  ? FloatingActionButton.extended(
                      onPressed: widget.onPress,
                      label: widget.text!,
                      icon: widget.icon,
                    )
                  : FloatingActionButton(
                      onPressed: widget.onPress,
                      child: widget.icon,
                    ),
            ),
          );
        },
      ),
    );
  }
}
  • ポイントは以下
          return SizedBox(
            height: widget.height,
            width: widget.height! + widthPercent * size,
            child: Transform.rotate(
              angle: widget.animateIcon! ? (3.6 * math.pi / 180) * size : 0,
              child: isFull
                  ? FloatingActionButton.extended(
                      onPressed: widget.onPress,
                      label: widget.text!,
                      icon: widget.icon,
                    )
                  : FloatingActionButton(
                      onPressed: widget.onPress,
                      child: widget.icon,
                    ),
            ),
          );

Discussion