flutteranimationclipflutter-clippath

Flutter - Hide a line that shouldn't be shown


I'm trying to create a text with an animation on hover, including a dot in front of it, using Flutter. The design comes from this website (look at the bottom section you'll find what I'm reproducing).

Here's the code for my component:

import 'package:flutter/material.dart';

class MyText extends StatefulWidget {
  final String text;
  final TextStyle textStyle;
  final double width;
  final double underlineOffset;
  final bool hasDot;
  const MyText({
    super.key,
    required this.text,
    required this.textStyle,
    required this.width,
    required this.underlineOffset,
    this.hasDot = false,
  });

  @override
  State<MyText> createState() => _MyTextState();
}

class _MyTextState extends State<MyText> with TickerProviderStateMixin {
  late AnimationController _colorAnimationController;
  late Animation _colorAnimation;
  late AnimationController _underlineAnimationController;
  late Animation _underlineAnimation;

  @override
  void initState() {
    _colorAnimationController = AnimationController(
      duration: const Duration(milliseconds: 250),
      vsync: this,
    )..addListener(() {
        setState(() {});
      });
    _colorAnimation = ColorTween(
      begin: Colors.black,
      end: Colors.orange,
    ).animate(_colorAnimationController);
    _underlineAnimationController = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    )..addListener(() {
        if (_underlineAnimationController.isCompleted &&
            _underlineAnimationController.value == 1) {
          _underlineAnimationController.reset();
        }
        setState(() {});
      });
    _underlineAnimation = Tween(begin: -widget.width, end: widget.width)
        .animate(_underlineAnimationController);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (event) {
        _colorAnimationController.forward();
        _underlineAnimationController.animateTo(0.5);
      },
      onExit: (event) {
        _colorAnimationController.reverse();
        _underlineAnimationController.animateTo(1);
      },
      cursor: SystemMouseCursors.click,
      child: ClipRRect(
        child: Container(
          alignment: Alignment.centerRight,
          width: widget.hasDot ? widget.width + 25 : widget.width,
          height: widget.underlineOffset + 2,
          // color: Colors.green,
          child: Stack(
            clipBehavior: Clip.none,
            children: [
              // Points (si il y en a un)
              if (widget.hasDot)
                const Positioned(
                  top: 9,
                  left: -26,
                  child: Icon(
                    Icons.circle,
                    color: Colors.black,
                    size: 10,
                  ),
                ),

              // Texte
              Stack(
                children: [
                  Text(
                    widget.text,
                    style: widget.textStyle.copyWith(
                      color: _colorAnimation.value,
                    ),
                  ),
                ],
              ),

              // Underline (onHover animation)
              Positioned(
                top: widget.underlineOffset,
                left: _underlineAnimation.value,
                child: Container(
                  width: widget.width,
                  height: 1,
                  color: Colors.orange,
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

It's all working fine, but when I set hasDot: true, the orange line appears under the dot.

Note: I want the underline animation to occur only under the text and not under the dot.

My issue is how to achieve the same animation effect as when hasDot is false, but with the dot included and the orange line hidden under the dot.

Additional info: If you need the full code of the Flutter project, please check my GitHub repo.

Thank you in advance for your help.

Have a great time coding!


Solution

  • Update: Before posting this question, I tried many ways to accomplish this without any success. After posting this question, I found the solution in about 2 seconds...

    So here is the full code of the component with the little modifications (basically added a Row to separate the dot from all the rest):

    import 'package:flutter/material.dart';
    import 'package:gap/gap.dart';
    import 'package:kanaknaturals_cursor/utilities/my_active_provider.dart';
    import 'package:provider/provider.dart';
    
    class MyText extends StatefulWidget {
      final int itemID;
      final String text;
      final TextStyle textStyle;
      final double width;
      final double underlineOffset;
      final bool hasDot;
      const MyText({
        Key key,
        required this.itemID,
        required this.text,
        required this.textStyle,
        required this.width,
        required this.underlineOffset,
        this.hasDot = false,
      });
    
      @override
      State<MyText> createState() => _MyTextState();
    }
    
    class _MyTextState extends State<MyText> with TickerProviderStateMixin {
      late AnimationController _colorAnimationController;
      late Animation _colorAnimation;
      late AnimationController _underlineAnimationController;
      late Animation _underlineAnimation;
    
      @override
      void initState() {
        _colorAnimationController = AnimationController(
          duration: const Duration(milliseconds: 250),
          vsync: this,
        )..addListener(() {
          setState(() {});
        });
        _colorAnimation = ColorTween(
          begin: Colors.black,
          end: Colors.orange,
        ).animate(_colorAnimationController);
        _underlineAnimationController = AnimationController(
          duration: const Duration(milliseconds: 500),
          vsync: this,
        )..addListener(() {
          if (_underlineAnimationController.isCompleted &&
              _underlineAnimationController.value == 1) {
            _underlineAnimationController.reset();
          }
          setState(() {});
        });
        _underlineAnimation = Tween(begin: -widget.width, end: widget.width)
            .animate(_underlineAnimationController);
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return MouseRegion(
          onEnter: (event) {
            _colorAnimationController.forward();
            _underlineAnimationController.animateTo(0.5);
            Provider.of<MyActiveProvider>(context, listen: false)
                .setActiveItem(widget.itemID);
          },
          onExit: (event) {
            _colorAnimationController.reverse();
            _underlineAnimationController.animateTo(1);
            Provider.of<MyActiveProvider>(context, listen: false)
                .setActiveItem(null);
          },
          cursor: SystemMouseCursors.click,
          child: Row(
            children: [
              // Points (if there is one)
              if (widget.hasDot)
                const Row(
                  children: [
                    Positioned(
                      top: 9,
                      left: -26,
                      child: Icon(
                        Icons.circle,
                        color: Colors.black,
                        size: 10,
                      ),
                    ),
                    Gap(10),
                  ],
                ),
              ClipRRect(
                child: Container(
                  alignment: Alignment.centerRight,
                  width: widget.hasDot ? widget.width : widget.width,
                  height: widget.underlineOffset + 2,
                  // color: Colors.green,
                  child: Stack(
                    clipBehavior: Clip.none,
                    children: [
                      // Text
                      Stack(
                        children: [
                          Text(
                            widget.text,
                            style: widget.textStyle.copyWith(
                              color: _colorAnimation.value,
                            ),
                          ),
                        ],
                      ),
    
                      // Underline (onHover animation)
                      Positioned(
                        top: widget.underlineOffset,
                        left: _underlineAnimation.value,
                        child: Container(
                          width: widget.width,
                          height: 1,
                          color: Colors.orange,
                        ),
                      )
                    ],
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }