(β)Log

Flutter UI Slice: Lazycatlabs Part 2

Published on
Authors

Background

I have created a web using Flutter Web, you can see on lazycatlabs.com also for Video Showcase you can see here. So I want to try to slice the UI component from what I use on my Flutter web.

Component

The second component I want to share is the simple one. The button animation, animating when you hover the pointer.

Create the component

In the Flutter, we have a playground to do simple code in here dartpad.dev

  • Click New Pad
  • Choose Flutter
  • And click Run button to check the result should be like this.

So the first one, we need to create a new class Animated Button and pass some arguments

class AnimatedButton extends StatefulWidget {
  const AnimatedButton({
    super.key,
    required this.onPressed,
    this.splashColor,
    this.startOffset = Offset.zero,
    this.targetOffset = const Offset(0.1, 0),
    this.startWidth = 45,
    this.targetWidth = 120,
    this.height = 45,
    this.child,
    this.color,
  });


  final double? startWidth;
  final double targetWidth;
  final double height;
  final Color? color;
  final Color? splashColor;
  final Offset startOffset;
  final Offset targetOffset;
  final Widget? child;
  final VoidCallback onPressed;

  
  State<AnimatedButton> createState() => _AnimatedButtonState();
}

class _AnimatedButtonState extends State<AnimatedButton>
    with SingleTickerProviderStateMixin {
  ...
}

When we are starting to create animation, we need TickerProvider class and that's why we add with SingleTickerProviderStateMixin to make our component support animation.

After that, we should create a controller to manage the animation

final animationDuration = const Duration(milliseconds:500);
late final AnimationController _animationController = AnimationController(
  duration: animationDuration,
  vsync: this,
)

For animation duration, we set it into 1 second, for the vsync: this, this is reference to TickerProvider from SingleTickerProviderStateMixin. And why I'm using late when create the variable, because when using late I don't need to initialize the data on initState method.

And the next, we need to create Offset Animation, because we need to move the button when cursor onHover to make animation smoother when background circle changed.

late final Animation<Offset> _offsetAnimation =
    Tween(begin: Offset.zero, end: const Offset(0.1,0))
        .animate(CurvedAnimation(parent: _animationController,curve: Curves.easeIn))..addListener((){
        setState((){});
        });

So, we create Animation variable will return Offset with start offset is Offset.zero and the end of animation to Offset(0.1,0) it's mean the button will be moving 0.1 by Horizontal, and we will use CurvedAnimation with parent from _animationController and use curve: Curves.easeIn, and also we need to refresh the page if animation is changed. You can try to change the value and see the different.

After that, we need to build the component using MouseRegion because we need to detect if the widget is hovering or not and use SlideTransition


/// to handle hover status
bool _isHovering = false;

void setOnHover({bool isHover = false}) {
  setState(() {
      _isHovering = isHover;
    if (isHover) {
    /// Starts running this animation forwards (towards the end).
      controller.forward();
    } else {
    /// Starts running this animation in reverse (towards the beginning).
      controller.reverse();
    }
  });
}

 
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => setOnHover(isHover: true),
      onExit: (_) => setOnHover(),
      child: SlideTransition(
        position: offsetAnimation,
        child: InkWell(
          hoverColor: Colors.transparent,
          onTap: widget.onPressed,
          onTapDown: (_) => setOnHover(isHover: true),
          onTapCancel: () => setOnHover(),
          onTapUp: (_) => setOnHover(),
          borderRadius: const BorderRadius.all(Radius.circular(80)),
          child: SizedBox(
            width: widget.targetWidth,
            height: widget.height,
            child: Stack(
              children: [
                /// You can choose circle position
                /// here I put the circle to right, you can remove it
                /// and replace with left:0
                Positioned(
                  right: 0,
                  child: AnimatedContainer(
                    duration: animationDuration,
                    width: _isHovering ? widget.targetWidth : widget.startWidth,
                    alignment: Alignment.centerLeft,
                    height: widget.height,
                    decoration: BoxDecoration(
                      color: widget.color ??
                          Theme.of(context).buttonTheme.colorScheme?.background,
                      borderRadius: const BorderRadius.all(Radius.circular(80)),
                    ),
                  ),
                ),
                Positioned(
                  top: widget.height / 3,
                  width: widget.targetWidth,
                  child: Center(
                    child: widget.child,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

Here for the result:

Full Code

Here for the full code

import 'package:flutter/material.dart';

class AnimatedButton extends StatefulWidget {
  final double? startWidth;
  final double targetWidth;
  final double height;
  final Color? color;
  final Color? splashColor;
  final Offset startOffset;
  final Offset targetOffset;
  final Widget? child;
  final VoidCallback onPressed;

  const AnimatedButton({
    super.key,
    required this.onPressed,
    this.splashColor,
    this.startOffset = Offset.zero,
    this.targetOffset = const Offset(0.1, 0),
    this.startWidth = 45,
    this.targetWidth = 120,
    this.height = 45,
    this.child,
    this.color,
  });

  
  State<AnimatedButton> createState() => _AnimatedButtonState();
}

class _AnimatedButtonState extends State<AnimatedButton>
    with SingleTickerProviderStateMixin {
  final animationDuration = const Duration(milliseconds: 500);
  late AnimationController controller = AnimationController(
    vsync: this,
    duration: animationDuration,
  );

  late Animation<Offset> offsetAnimation = Tween<Offset>(
    begin: widget.startOffset,
    end: widget.targetOffset,
  ).animate(CurvedAnimation(parent: controller, curve: Curves.easeIn))
    ..addListener(() {
      setState(() {});
    });

  bool _isHovering = false;

  void setOnHover({bool isHover = false}) {
    setState(() {
      _isHovering = isHover;
      if (isHover) {
        controller.forward();
      } else {
        controller.reverse();
      }
    });
  }

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

  
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => setOnHover(isHover: true),
      onExit: (_) => setOnHover(),
      child: SlideTransition(
        position: offsetAnimation,
        child: InkWell(
          hoverColor: Colors.transparent,
          onTap: widget.onPressed,
          onTapDown: (_) => setOnHover(isHover: true),
          onTapCancel: () => setOnHover(),
          onTapUp: (_) => setOnHover(),
          borderRadius: const BorderRadius.all(Radius.circular(80)),
          child: SizedBox(
            width: widget.targetWidth,
            height: widget.height,
            child: Stack(
              children: [
                Positioned(
                  right: 0,
                  child: AnimatedContainer(
                    duration: animationDuration,
                    width: _isHovering ? widget.targetWidth : widget.startWidth,
                    alignment: Alignment.centerLeft,
                    height: widget.height,
                    decoration: BoxDecoration(
                      color: widget.color ??
                          Theme.of(context).buttonTheme.colorScheme?.background,
                      borderRadius: const BorderRadius.all(Radius.circular(80)),
                    ),
                  ),
                ),
                Positioned(
                  top: widget.height / 3,
                  width: widget.targetWidth,
                  child: Center(
                    child: widget.child,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

If you have any questions, feel free to write your comment and don't forget to add reactions if you like this article.