본문 바로가기

flutter

expandable fab

https://docs.flutter.dev/cookbook/effects/expandable-fab

 

Create an expandable FAB

How to implement a FAB that expands to multiple buttons when tapped.

docs.flutter.dev

1. create an expandable fab

@immutable
class ExpandableFab extends StatefulWidget {
  const ExpandableFab({
    super.key,
    this.initialOpen,
    required this.distance,
    required this.children,
  });

  final bool? initialOpen;
  final double distance;
  final List<Widget> children;

  @override
  _ExpandableFabState createState() => _ExpandableFabState();
}

class _ExpandableFabState extends State<ExpandableFab> {
  @override
  Widget build(BuildContext context) {
    return const SizedBox();
  }
}

initialOpen : 첫 open 여부

distance : floating actionbutton과 자식 widget 사이 거리

children : 자식 위젯 -> 펼쳤을 때 생기는 위젯

 

2. FAB cross-fade

class _ExpandableFabState extends State<ExpandableFab> {
  bool _open = false;

  @override
  void initState() {
    super.initState();
    _open = widget.initialOpen ?? false;
  }

  void _toggle() {
    setState(() {
      _open = !_open;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand(
      child: Stack(
        alignment: Alignment.bottomRight,
        clipBehavior: Clip.none,
        children: [
          _buildTapToCloseFab(),
          _buildTapToOpenFab(),
        ],
      ),
    );
  }

  Widget _buildTapToCloseFab() {
    return SizedBox(
      width: 56.0,
      height: 56.0,
      child: Center(
        child: Material(
          shape: const CircleBorder(),
          clipBehavior: Clip.antiAlias,
          elevation: 4.0,
          child: InkWell(
            onTap: _toggle,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Icon(
                Icons.close,
                color: Theme.of(context).primaryColor,
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildTapToOpenFab() {
    return IgnorePointer(
      ignoring: _open,
      child: AnimatedContainer(
        transformAlignment: Alignment.center,
        transform: Matrix4.diagonal3Values(
          _open ? 0.7 : 1.0,
          _open ? 0.7 : 1.0,
          1.0,
        ),
        duration: const Duration(milliseconds: 250),
        curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
        child: AnimatedOpacity(
          opacity: _open ? 0.0 : 1.0,
          curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
          duration: const Duration(milliseconds: 250),
          child: FloatingActionButton(
            onPressed: _toggle,
            child: const Icon(Icons.create),
          ),
        ),
      ),
    );
  }
}

Stack에서 열기버튼은 닫기 버튼 위에 위치한다. 버튼을 클릭하여 나타나고 사라질 때 크로스 페이드의 시각적 모양을 허용한다. IgnorePointer를 통해서 위에 위치한 열기버튼이 사라졌을때 (_open = true일 때) 클릭을 막고 아래의 닫기 버튼을 클릭할 수 있도록 한다.

 

3. ActionButton 위젯 만들기

확장했을 때 생길 자식 버튼

class ActionButton extends StatelessWidget {
  const ActionButton({
    super.key,
    this.onPressed,
    required this.icon,
  });

  final VoidCallback? onPressed;
  final Widget icon;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Material(
      shape: const CircleBorder(),
      clipBehavior: Clip.antiAlias,
      color: theme.colorScheme.secondary,
      elevation: 4.0,
      child: IconButton(
        onPressed: onPressed,
        icon: icon,
        color: theme.colorScheme.secondary,
      ),
    );
  }
}

 

4. ActionButton 확장 및 축소

ActionButton은 확장될 때 열린 FAB 아래에서 날아가야 하고 접힐 때 열린 FAB 아래로 날아가야 한다. AnimationController 및 Animation을 사용하여 다양한 ActionButton이 확장 및 축소되는 속도를 제어한다.

class _ExpandableFabState extends State<ExpandableFab>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _expandAnimation;
  bool _open = false;

  @override
  void initState() {
    super.initState();
    _open = widget.initialOpen ?? false;
    _controller = AnimationController(
      value: _open ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 250),
      vsync: this,
    );
    _expandAnimation = CurvedAnimation(
      curve: Curves.fastOutSlowIn,
      reverseCurve: Curves.easeOutQuad,
      parent: _controller,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggle() {
    setState(() {
      _open = !_open;
      if (_open) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }
}

ActionButton의 위치 조건은 아래와 같다.

1. degree (90도 안에)

2. 거리

3. 애니메이션

 

class _ExpandableActionButton extends StatelessWidget {
  const _ExpandableActionButton(
      {Key? key,
      required this.distance,
      required this.degree,
      required this.progress,
      required this.child})
      : super(key: key);

  final double distance;
  final double degree;
  final Animation<double> progress;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: progress,
        builder: (BuildContext context, Widget? child) {
          final Offset offset = Offset.fromDirection(degree * (pi / 180), progress.value * distance); //진행률을 distance에 곱해서 위치 지정
          return Positioned(
            right: offset.dx + 4,
            bottom: offset.dy + 4,
            child: Transform.rotate(
              angle: (1.0 - progress.value) * pi / 2,
              child: child!,
            ),
          );
        },
        child: child);
  }
}

degree, distance 그리고 애니메이션 진행률(progress.value)을 이용해서 Offset을 구한 다음 Positioned 위젯의 위치를 지정한다.

 

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand(
      child: Stack(
        alignment: Alignment.bottomRight,
        clipBehavior: Clip.none,
        children: [
          _buildTapToCloseFab(),
          _buildTapToOpenFab(),
        ]..insertAll(0, _buildExpandableActionButton()),
      ),
    );
  }
  
   List<_ExpandableActionButton> _buildExpandableActionButton() {
    List<_ExpandableActionButton> animChildren = [];
    final int count = widget.children.length;
    final double gap = 90.0 / (count - 1);

    for (var i = 0, degree = 0.0; i < count; i++, degree += gap) {
      animChildren.add(_ExpandableActionButton(
        distance: widget.distance,
        progress: _expandAnimation,
        degree: degree,
        child: widget.children[i],
      ));
    }
    return animChildren;
  }

 

'flutter' 카테고리의 다른 글

freezed live template로 쉽게 생성하기  (0) 2022.08.11
context 없이 navigation 구현하기  (0) 2022.08.05
singleton  (0) 2022.06.14
flutter 해당 위젯 코드 쉽게 찾기  (0) 2022.05.31
BoxConstraints forces an infinite width  (0) 2022.05.26