From 5ebe6f08b2b6cadb16818959baa5ecbf55d10141 Mon Sep 17 00:00:00 2001 From: Victor HG Date: Sat, 9 May 2020 10:38:22 -0500 Subject: [PATCH] * Added animation when dismissing https://github.com/victorevox/simple_tooltp/issues/6 * Fix borderWidth=0 not honoured https://github.com/victorevox/simple_tooltp/issues/7 * Added [hideOnTooltipTap] capability, requested https://github.com/victorevox/simple_tooltp/issues/5 * Updated README * Other fixes --- CHANGELOG.md | 8 ++ README.md | 8 ++ example/lib/animated_example_page.dart | 153 ++++++++++++++++++++ example/lib/basics_example_page.dart | 90 ++++++++++++ example/lib/main.dart | 189 ++++--------------------- example/pubspec.lock | 2 +- lib/src/ballon_transition.dart | 81 +++++++---- lib/src/balloon.dart | 3 +- lib/src/balloon_positioner.dart | 14 ++ lib/src/tooltip.dart | 66 ++++++--- pubspec.yaml | 2 +- 11 files changed, 413 insertions(+), 203 deletions(-) create mode 100644 example/lib/animated_example_page.dart create mode 100644 example/lib/basics_example_page.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 11dae3c..6360cd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [0.1.7] - 9/05/20. + +* Added animation when dismissing https://github.com/victorevox/simple_tooltp/issues/6 +* Fix borderWidth=0 not honoured https://github.com/victorevox/simple_tooltp/issues/7 +* Added [hideOnTooltipTap] capability, requested https://github.com/victorevox/simple_tooltp/issues/5 +* Updated README +* Other fixes + ## [0.1.6] - 9/05/20. * Ensure tooltip is hidden when dispossing (fix: https://github.com/victorevox/simple_tooltp/issues/9) diff --git a/README.md b/README.md index 2741bb3..af9b837 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,16 @@ SimpleTooltip( /// defaults to `const BoxShadow(color: const Color(0x45222222), blurRadius: 8, spreadRadius: 2),` final List customShadows; + /// + /// Set a handler for listening to a `tap` event on the tooltip (This is the recommended way to put your logic for dismissing the tooltip) final Function() tooltipTap; + /// + /// If you want to automatically dismiss the tooltip whenever a user taps on it, set this flag to [true] + /// For more control on when to dismiss the tooltip please rely on the [show] property and [tooltipTap] handler + /// defaults to [false] + final bool hideOnTooltipTap; + ``` ## Screenshots diff --git a/example/lib/animated_example_page.dart b/example/lib/animated_example_page.dart new file mode 100644 index 0000000..59576da --- /dev/null +++ b/example/lib/animated_example_page.dart @@ -0,0 +1,153 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:simple_tooltip/simple_tooltip.dart'; + +class AnimatedExamplePage extends StatefulWidget { + AnimatedExamplePage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _AnimatedExamplePageState createState() => _AnimatedExamplePageState(); +} + +class _AnimatedExamplePageState extends State { + AnimationStatus _marginAnimationStatus; + + int _restartCount = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MarginTransition( + animationStatusChange: (status) { + setState(() { + _marginAnimationStatus = status; + if (status == AnimationStatus.forward) { + _restartCount++; + } + if (_restartCount > 4) { + _restartCount = 0; + } + }); + }, + child: Container( + width: 280, + height: 120, + child: Placeholder(), + ), + builder: (context, margin, child) { + // print(_marginAnimationStatus); + return Container( + margin: _marginAnimationStatus == AnimationStatus.forward + ? EdgeInsets.only(left: margin) + : EdgeInsets.only(right: margin), + child: SimpleTooltip( + tooltipTap: () { + print("tooltip tap ${Random().nextDouble()}"); + }, + backgroundColor: + _marginAnimationStatus == AnimationStatus.forward + ? Colors.white + : Colors.blue[300], + borderColor: + _marginAnimationStatus == AnimationStatus.forward + ? Colors.purple + : Colors.orange, + show: true, + arrowTipDistance: 10, + tooltipDirection: _restartCount > 2 + ? _marginAnimationStatus == AnimationStatus.reverse + ? TooltipDirection.up + : TooltipDirection.down + : _marginAnimationStatus == AnimationStatus.reverse + ? TooltipDirection.right + : TooltipDirection.left, + child: child, + content: Text( + "Some text example!!!!", + style: TextStyle( + color: Colors.black, + fontSize: 18, + decoration: TextDecoration.none, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ); + } +} + +class MarginTransition extends StatefulWidget { + final Widget child; + final Widget Function(BuildContext, double margin, Widget child) builder; + final ValueChanged animationStatusChange; + + MarginTransition({ + Key key, + @required this.child, + @required this.builder, + this.animationStatusChange, + }) : assert(builder != null), + super(key: key); + + @override + _MarginTransitionState createState() => _MarginTransitionState(); +} + +class _MarginTransitionState extends State + with SingleTickerProviderStateMixin { + AnimationController _animationController; + Animation _animation; + + @override + void initState() { + super.initState(); + _animationController = + AnimationController(vsync: this, duration: Duration(seconds: 4)); + _animationController.forward(); + _animation = + Tween(begin: 10, end: 300).animate(_animationController) + ..addStatusListener((status) { + if (status == AnimationStatus.completed) { + _animationController.reverse(); + } else if (status == AnimationStatus.dismissed) { + _animationController.forward(); + } + + if (widget.animationStatusChange != null) { + widget.animationStatusChange(_animationController.status); + } + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + child: widget.child, + builder: (context, child) { + return widget.builder(context, _animation.value, child); + }, + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} diff --git a/example/lib/basics_example_page.dart b/example/lib/basics_example_page.dart new file mode 100644 index 0000000..5a061b0 --- /dev/null +++ b/example/lib/basics_example_page.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:simple_tooltip/simple_tooltip.dart'; + +class BasicsExamplePage extends StatefulWidget { + const BasicsExamplePage({Key key}) : super(key: key); + + @override + _BasicsExamplePageState createState() => _BasicsExamplePageState(); +} + +class _BasicsExamplePageState extends State { + bool _show = false; + bool hideOnTap = false; + TooltipDirection _direction = TooltipDirection.up; + bool _changeBorder = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Basics"), + ), + body: Center( + child: Container( + child: Column( + children: [ + RaisedButton( + child: Text("change direction"), + onPressed: () { + setState(() { + switch (_direction) { + case TooltipDirection.up: + _direction = TooltipDirection.right; + break; + case TooltipDirection.right: + _direction = TooltipDirection.down; + break; + case TooltipDirection.down: + _direction = TooltipDirection.left; + break; + case TooltipDirection.left: + _direction = TooltipDirection.up; + break; + default: + } + }); + }, + ), + RaisedButton( + child: Text("toogle: $_show"), + onPressed: () { + setState(() { + _show = !_show; + }); + }, + ), + RaisedButton( + child: Text("hideOnTap: $hideOnTap"), + onPressed: () { + setState(() { + hideOnTap = !hideOnTap; + }); + }, + ), + RaisedButton( + child: Text("change border: $hideOnTap"), + onPressed: () { + setState(() { + _changeBorder = !_changeBorder; + }); + }, + ), + SimpleTooltip( + show: _show, + tooltipDirection: _direction, + hideOnTooltipTap: hideOnTap, + borderWidth: _changeBorder? 0 : 3, + child: Container( + color: Colors.cyan, + width: 80, + height: 80, + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 356c334..e5219d3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,9 @@ import 'dart:math'; +import 'package:example/basics_example_page.dart'; import 'package:flutter/material.dart'; import 'package:simple_tooltip/simple_tooltip.dart'; +import 'animated_example_page.dart'; void main() => runApp(MyApp()); @@ -14,177 +16,48 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'Flutter Demo Home Page'), + home: OptionsPage(), ); } } -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - AnimationStatus _marginAnimationStatus; - - int _restartCount = 0; +class OptionsPage extends StatelessWidget { + const OptionsPage({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: Text("Simple Tooltip"), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // SimpleTooltip( - // tooltipTap: () { - // print("Tooltip tap"); - // }, - // animationDuration: Duration(seconds: 3), - // show: true, - // tooltipDirection: TooltipDirection.up, - // child: Container( - // width: 200, - // height: 120, - // child: Placeholder(), - // ), - // content: Text( - // "Some text example!!!!", - // style: TextStyle( - // color: Colors.black, - // fontSize: 18, - // decoration: TextDecoration.none, - // ), - // ), - // ), - MarginTransition( - animationStatusChange: (status) { - setState(() { - _marginAnimationStatus = status; - if (status == AnimationStatus.forward) { - _restartCount++; - } - if (_restartCount > 4) { - _restartCount = 0; - } - }); - }, - child: Container( - width: 280, - height: 120, - child: Placeholder(), + body: Container( + height: double.infinity, + width: double.maxFinite, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RaisedButton( + child: Text("Animated example"), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (ctx) => + AnimatedExamplePage(title: 'Flutter Demo Home Page'), + )); + }, + ), + RaisedButton( + child: Text("Basics"), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (ctx) => BasicsExamplePage(), + )); + }, ), - builder: (context, margin, child) { - // print(_marginAnimationStatus); - return Container( - margin: _marginAnimationStatus == AnimationStatus.forward - ? EdgeInsets.only(left: margin) - : EdgeInsets.only(right: margin), - child: SimpleTooltip( - tooltipTap: () { - print("tooltip tap ${Random().nextDouble()}"); - }, - backgroundColor: - _marginAnimationStatus == AnimationStatus.forward - ? Colors.white - : Colors.blue[300], - borderColor: - _marginAnimationStatus == AnimationStatus.forward - ? Colors.purple - : Colors.orange, - show: true, - arrowTipDistance: 10, - tooltipDirection: _restartCount > 2 - ? _marginAnimationStatus == AnimationStatus.reverse - ? TooltipDirection.up - : TooltipDirection.down - : _marginAnimationStatus == AnimationStatus.reverse - ? TooltipDirection.right - : TooltipDirection.left, - child: child, - content: Text( - "Some text example!!!!", - style: TextStyle( - color: Colors.black, - fontSize: 18, - decoration: TextDecoration.none, - ), - ), - ), - ); - }, - ), - ], + ], + ), ), ), ); } } - -class MarginTransition extends StatefulWidget { - final Widget child; - final Widget Function(BuildContext, double margin, Widget child) builder; - final ValueChanged animationStatusChange; - - MarginTransition({ - Key key, - @required this.child, - @required this.builder, - this.animationStatusChange, - }) : assert(builder != null), - super(key: key); - - @override - _MarginTransitionState createState() => _MarginTransitionState(); -} - -class _MarginTransitionState extends State - with SingleTickerProviderStateMixin { - AnimationController _animationController; - Animation _animation; - - @override - void initState() { - super.initState(); - _animationController = - AnimationController(vsync: this, duration: Duration(seconds: 4)); - _animationController.forward(); - _animation = - Tween(begin: 10, end: 300).animate(_animationController) - ..addStatusListener((status) { - if (status == AnimationStatus.completed) { - _animationController.reverse(); - } else if (status == AnimationStatus.dismissed) { - _animationController.forward(); - } - - if (widget.animationStatusChange != null) { - widget.animationStatusChange(_animationController.status); - } - }); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _animation, - child: widget.child, - builder: (context, child) { - return widget.builder(context, _animation.value, child); - }, - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } -} diff --git a/example/pubspec.lock b/example/pubspec.lock index 76b8ad1..9756e9b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -122,7 +122,7 @@ packages: path: ".." relative: true source: path - version: "0.1.6" + version: "0.1.7" sky_engine: dependency: transitive description: flutter diff --git a/lib/src/ballon_transition.dart b/lib/src/ballon_transition.dart index 1674fe8..fe77248 100644 --- a/lib/src/ballon_transition.dart +++ b/lib/src/ballon_transition.dart @@ -4,12 +4,16 @@ class _BalloonTransition extends StatefulWidget { final Widget child; final Duration duration; final TooltipDirection tooltipDirection; + final bool hide; + final Function(AnimationStatus) animationEnd; _BalloonTransition({ Key key, @required this.child, @required this.duration, @required this.tooltipDirection, + this.hide = false, + this.animationEnd, }) : super(key: key); @override @@ -24,36 +28,61 @@ class _BalloonTransitionState extends State<_BalloonTransition> @override void initState() { super.initState(); - _animationController = - AnimationController(vsync: this, duration: widget.duration); - final CurvedAnimation curvedAnimation = - CurvedAnimation(curve: Curves.bounceOut, parent: _animationController); - _rotationAnimation = - Tween(begin: pi * .6, end: 0).animate(curvedAnimation); - _animationController.forward(); + _animationController = AnimationController( + vsync: this, + duration: widget.duration, + ); + final CurvedAnimation curvedAnimation = CurvedAnimation( + curve: Curves.bounceOut, + parent: _animationController, + ); + _rotationAnimation = Tween(begin: pi * .5, end: 0).animate( + curvedAnimation, + ); + if (!widget.hide) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + _animationController.addStatusListener((status) { + if ((status == AnimationStatus.completed || + status == AnimationStatus.dismissed) && + widget.animationEnd != null) { + widget.animationEnd(status); + } + }); + } + + @override + void didUpdateWidget(_BalloonTransition oldWidget) { + if (widget.hide) { + _animationController.reverse(); + } + super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return _OpacityAnimationWrapper( - duration: widget.duration, - child: AnimatedBuilder( - animation: _rotationAnimation, - builder: (context, _) { - // print(_rotationAnimation.value); - _BallonTransformation _ballonTransformation = - _BallonTransformation.forAnimationValue( - _rotationAnimation.value, - widget.tooltipDirection, - ); - return Transform( - child: widget.child, - // transform: Matrix4.identity()..setEntry(1, 2, _rotationAnimation.value), - alignment: _ballonTransformation.alignment, - transform: _ballonTransformation.transformation, - ); - }, - )); + duration: widget.duration, + child: AnimatedBuilder( + animation: _rotationAnimation, + builder: (context, _) { + // print(_rotationAnimation.value); + _BallonTransformation _ballonTransformation = + _BallonTransformation.forAnimationValue( + _rotationAnimation.value, + widget.tooltipDirection, + ); + return Transform( + child: widget.child, + // transform: Matrix4.identity()..setEntry(1, 2, _rotationAnimation.value), + alignment: _ballonTransformation.alignment, + transform: _ballonTransformation.transformation, + ); + }, + ), + ); } @override @@ -118,7 +147,7 @@ class __OpacityAnimationWrapperState extends State<_OpacityAnimationWrapper> { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if(mounted) { + if (mounted) { setState(() { _opacity = 1; }); diff --git a/lib/src/balloon.dart b/lib/src/balloon.dart index dda5fbc..01526c1 100644 --- a/lib/src/balloon.dart +++ b/lib/src/balloon.dart @@ -257,7 +257,8 @@ class _BalloonShape extends ShapeBorder { @override void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { Paint paint = new Paint() - ..color = borderColor + // if borderWidth is set to 0, set the color to be transparent to avoid border to be visible because strange behavior + ..color = borderWidth == 0? Color(0x00000000) : borderColor ..style = PaintingStyle.stroke ..strokeWidth = borderWidth; diff --git a/lib/src/balloon_positioner.dart b/lib/src/balloon_positioner.dart index b32ede1..9c37eac 100644 --- a/lib/src/balloon_positioner.dart +++ b/lib/src/balloon_positioner.dart @@ -53,6 +53,15 @@ class __BallonPositionerState extends State<_BallonPositioner> { super.initState(); } + @override + void didUpdateWidget (_BallonPositioner oldWidget) { + if(widget.tooltipDirection != oldWidget.tooltipDirection) { + // invalidate ballon size to perform recalculation + _ballonSize = null; + } + super.didUpdateWidget(oldWidget); + } + @override Widget build(BuildContext _) { if (widget.context == null) { @@ -86,6 +95,11 @@ class __BallonPositionerState extends State<_BallonPositioner> { ancestor: overlay, ); + final debugg = renderBox.localToGlobal( + renderBox.size.center(zeroOffset), + ancestor: overlay, + ); + final balloon = CustomSingleChildLayout( delegate: _PopupBallonLayoutDelegate( arrowLength: widget.arrowLength, diff --git a/lib/src/tooltip.dart b/lib/src/tooltip.dart index a157da9..041e523 100644 --- a/lib/src/tooltip.dart +++ b/lib/src/tooltip.dart @@ -79,8 +79,16 @@ class SimpleTooltip extends StatefulWidget { /// defaults to `const BoxShadow(color: const Color(0x45222222), blurRadius: 8, spreadRadius: 2),` final List customShadows; + /// + /// Set a handler for listening to a `tap` event on the tooltip (This is the recommended way to put your logic for dismissing the tooltip) final Function() tooltipTap; + /// + /// If you want to automatically dismiss the tooltip whenever a user taps on it, set this flag to [true] + /// For more control on when to dismiss the tooltip please rely on the [show] property and [tooltipTap] handler + /// defaults to [false] + final bool hideOnTooltipTap; + SimpleTooltip({ Key key, this.child, @@ -108,6 +116,7 @@ class SimpleTooltip extends StatefulWidget { color: const Color(0x45222222), blurRadius: 8, spreadRadius: 2), ], this.tooltipTap, + this.hideOnTooltipTap = false, }) : assert(show != null), super(key: key); @@ -126,9 +135,9 @@ class _SimpleTooltipState extends State { OverlayEntry overlayEntry; - @override + @override void dispose() { - _hideTooltip(); + _removeTooltip(); super.dispose(); } @@ -145,24 +154,25 @@ class _SimpleTooltipState extends State { @override void didUpdateWidget(SimpleTooltip oldWidget) { - super.didUpdateWidget(oldWidget); - _hideTooltip(); WidgetsBinding.instance.addPostFrameCallback((_) { - if (oldWidget.tooltipDirection != widget.tooltipDirection) { + if (oldWidget.tooltipDirection != widget.tooltipDirection || (oldWidget.show != widget.show && widget.show)) { _transitionKey = GlobalKey(); } + _removeTooltip(); if (widget.show) { _showTooltip(); - } else { - _hideTooltip(); + } else if (oldWidget.show) { + _showTooltip(buildHidding: true); + // setState(() { + // }); } }); + super.didUpdateWidget(oldWidget); } @override void didChangeDependencies() { super.didChangeDependencies(); - // _hideTooltip(); } @override @@ -173,16 +183,24 @@ class _SimpleTooltipState extends State { ); } - void _showTooltip() { + void _showTooltip({ + bool buildHidding = false, + }) { if (_displaying || !mounted) { return; } - this.overlayEntry = this._buildOverlay(); - Overlay.of(context).insert(this.overlayEntry); - this._displaying = true; + overlayEntry = _buildOverlay( + buildHidding: buildHidding, + ); + Overlay.of(context).insert(overlayEntry); + _displaying = true; } - void _hideTooltip() { + // void _hideToolTip() { + // _hide = true; + // } + + void _removeTooltip() { if (!_displaying) { return; } @@ -190,7 +208,9 @@ class _SimpleTooltipState extends State { _displaying = false; } - OverlayEntry _buildOverlay() { + OverlayEntry _buildOverlay({ + bool buildHidding = false, + }) { return OverlayEntry( builder: (overlayContext) { return _BallonPositioner( @@ -205,6 +225,12 @@ class _SimpleTooltipState extends State { key: _transitionKey, duration: widget.animationDuration, tooltipDirection: widget.tooltipDirection, + hide: buildHidding, + animationEnd: (status) { + if (status == AnimationStatus.dismissed) { + _removeTooltip(); + } + }, child: _Ballon( content: widget.content, borderRadius: widget.borderRadius, @@ -217,7 +243,15 @@ class _SimpleTooltipState extends State { tooltipDirection: widget.tooltipDirection, backgroundColor: widget.backgroundColor, shadows: widget.customShadows, - onTap: widget.tooltipTap, + onTap: () { + if (widget.hideOnTooltipTap) { + _removeTooltip(); + _showTooltip(buildHidding: true); + } + if (widget.tooltipTap != null) { + widget.tooltipTap(); + } + }, ), ), // arrowBaseWidth: widget.arrowBaseWidth, @@ -229,4 +263,4 @@ class _SimpleTooltipState extends State { }, ); } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 7b1fa76..a934956 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: simple_tooltip description: A simple library for creating tooltips -version: 0.1.6 +version: 0.1.7 homepage: https://github.com/victorevox/simple_tooltp environment: