From a018ebfeb4526f87c844f9f2bc9f4beba6e7db8a Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Thu, 14 Oct 2021 16:35:32 -0600 Subject: [PATCH 01/16] feat: enableExtraSpace & new positioning system --- lib/src/slide.dart | 255 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 245 insertions(+), 10 deletions(-) diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 3ac7e40..2e9ca25 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -274,10 +274,17 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { /// The child direction. final AxisDirection direction; + /// Enables a new positioning system that will allow the child of the slide to + /// expand beyond the highlighted area's dimensions. + /// + /// Heavily assisted by using an `Align` widget to align it within the expanded space + final bool enableExtraSpace; + /// Creates a new relative bubble slide child instance. const RelativeBubbleSlideChild({ required Widget widget, this.direction = AxisDirection.down, + this.enableExtraSpace = false, }) : super( widget: widget, ); @@ -288,31 +295,259 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { Position highlightPosition, Size parentSize, ) { - switch (direction) { - case AxisDirection.up: + if (enableExtraSpace) { + int quadrant = + _getQuadrantFromRelativePosition(highlightPosition, parentSize); + + switch (direction) { + case AxisDirection.up: + return _getUpPositionFromQuadrant( + quadrant, + parentSize, + highlightPosition, + ); + case AxisDirection.right: + return _getRightPositionFromQuadrant( + quadrant, + parentSize, + highlightPosition, + ); + case AxisDirection.left: + return _getLeftPositionFromQuadrant( + quadrant, + parentSize, + highlightPosition, + ); + default: + return _getDownPositionFromQuadrant( + quadrant, parentSize, highlightPosition); + } + } else { + switch (direction) { + case AxisDirection.up: + return Position( + right: parentSize.width - highlightPosition.right, + bottom: parentSize.height - highlightPosition.top, + left: highlightPosition.left, + ); + case AxisDirection.right: + return Position( + top: highlightPosition.top, + bottom: parentSize.height - highlightPosition.bottom, + right: parentSize.width - highlightPosition.left, + ); + case AxisDirection.left: + return Position( + top: highlightPosition.top, + bottom: parentSize.height - highlightPosition.bottom, + left: highlightPosition.right, + ); + default: + return Position( + top: highlightPosition.bottom, + right: parentSize.width - highlightPosition.right, + left: highlightPosition.left, + ); + } + } + } + + int _getQuadrantFromRelativePosition( + Position highlightPosition, + Size parentSize, + ) { + final r = highlightPosition.right; + final l = highlightPosition.left; + final t = highlightPosition.top; + final b = highlightPosition.bottom; + + final p = parentSize; + + if (l >= p.width / 2 && b <= p.height / 2) { + return 1; // top right + } else if (r <= p.width / 2 && b <= p.height / 2) { + return 2; // top left + } else if (r <= p.width / 2 && t >= p.height / 2) { + return 3; // bottom right + } else if (l >= p.width / 2 && t >= p.height / 2) { + return 4; // bottom left + } else { + return 5; // center (Not totally within any other quadrant) + } + } + + Position _getDownPositionFromQuadrant( + int quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case 1: + case 4: + // It will expand to the left + final spacingFromTheRightEdge = + parentSize.width - highlightPosition.right; + print(highlightPosition); + return Position( + right: spacingFromTheRightEdge, + top: highlightPosition.bottom, + ); + case 2: + case 3: + // It will expand to the right return Position( - right: parentSize.width - highlightPosition.right, - bottom: parentSize.height - highlightPosition.top, left: highlightPosition.left, + top: highlightPosition.bottom, ); - case AxisDirection.right: + case 5: + final widthFromRightEdge = parentSize.width - highlightPosition.right; + final widthFromLeftEdge = highlightPosition.left; + final availableWidth = widthFromRightEdge > widthFromLeftEdge + ? highlightPosition.right + : highlightPosition.left; + + return Position( + top: highlightPosition.bottom, + right: widthFromRightEdge > widthFromLeftEdge + ? (widthFromRightEdge) - (availableWidth / 2) + : availableWidth / 2, + left: widthFromLeftEdge > widthFromRightEdge + ? (widthFromLeftEdge) + (availableWidth / 2) + : availableWidth / 2, + ); + default: + throw ("Positioning is not optimal"); + } + } + + Position _getLeftPositionFromQuadrant( + int quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case 1: + case 2: + // It will expand to the bottom return Position( top: highlightPosition.top, - bottom: parentSize.height - highlightPosition.bottom, right: parentSize.width - highlightPosition.left, ); - case AxisDirection.left: + case 3: + case 4: + // It will expand to the top + return Position( + bottom: parentSize.height - highlightPosition.top, + right: parentSize.width - highlightPosition.left, + ); + case 5: + // It will be centered + final topHeightFromEdge = highlightPosition.top; + final bottomHeightFromEdge = + parentSize.height - highlightPosition.bottom; + final availableHeight = topHeightFromEdge > bottomHeightFromEdge + ? parentSize.height - highlightPosition.bottom + : parentSize.height - highlightPosition.top; + + return Position( + top: topHeightFromEdge > bottomHeightFromEdge + ? (topHeightFromEdge) - (availableHeight / 2) + : 0, + bottom: bottomHeightFromEdge > topHeightFromEdge + ? (bottomHeightFromEdge) + (availableHeight / 2) + : availableHeight / 2, + right: highlightPosition.right, + ); + + default: + throw ("Slide is outside the view area"); + } + } + + Position _getRightPositionFromQuadrant( + int quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case 1: + case 2: + // It will expand to the bottom return Position( top: highlightPosition.top, - bottom: parentSize.height - highlightPosition.bottom, + left: highlightPosition.right, + ); + case 3: + case 4: + // It will expand to the top + return Position( + bottom: parentSize.height - highlightPosition.top, + left: highlightPosition.right, + ); + case 5: + // It will be centered + final topHeightFromEdge = highlightPosition.top; + final bottomHeightFromEdge = + parentSize.height - highlightPosition.bottom; + final availableHeight = topHeightFromEdge > bottomHeightFromEdge + ? parentSize.height - highlightPosition.bottom + : parentSize.height - highlightPosition.top; + + return Position( + top: topHeightFromEdge > bottomHeightFromEdge + ? (topHeightFromEdge) - (availableHeight / 2) + : 0, + bottom: bottomHeightFromEdge > topHeightFromEdge + ? (bottomHeightFromEdge) + (availableHeight / 2) + : availableHeight / 2, left: highlightPosition.right, ); default: + throw ("Slide is outside the view area"); + } + } + + Position _getUpPositionFromQuadrant( + int quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case 1: + case 4: + // It will expand to the left + final spacingFromTheRightEdge = + parentSize.width - highlightPosition.right; + print(highlightPosition); + return Position( + right: spacingFromTheRightEdge, + bottom: parentSize.height - highlightPosition.top, + ); + case 2: + case 3: + // It will expand to the right return Position( - top: highlightPosition.bottom, - right: parentSize.width - highlightPosition.right, left: highlightPosition.left, + bottom: parentSize.height - highlightPosition.top, + ); + case 5: + final widthFromRightEdge = parentSize.width - highlightPosition.right; + final widthFromLeftEdge = highlightPosition.left; + final availableWidth = widthFromRightEdge > widthFromLeftEdge + ? highlightPosition.right + : highlightPosition.left; + + return Position( + bottom: parentSize.height - highlightPosition.top, + right: widthFromRightEdge > widthFromLeftEdge + ? (widthFromRightEdge) - (availableWidth / 2) + : availableWidth / 2, + left: widthFromLeftEdge > widthFromRightEdge + ? (widthFromLeftEdge) + (availableWidth / 2) + : availableWidth / 2, ); + default: + throw ("Slide is outside the view area"); } } } From d847dfdf3c585643c1204ba98a1021450a68ed0f Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Thu, 14 Oct 2021 16:35:50 -0600 Subject: [PATCH 02/16] chore: update example --- example/lib/main.dart | 354 +++++++++++++++++++++++++++++++++++------- 1 file changed, 301 insertions(+), 53 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 3fe2eee..a1462c9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -23,13 +23,17 @@ class _BubbleShowcaseDemoApp extends StatelessWidget { /// The main demo widget. class _BubbleShowcaseDemoWidget extends StatelessWidget { /// The title text global key. - final GlobalKey _titleKey = GlobalKey(); + final GlobalKey _firstSlideKey = GlobalKey(); /// The first button global key. - final GlobalKey _firstButtonKey = GlobalKey(); + final GlobalKey _secondSlideKey = GlobalKey(); /// The second button global key. - final GlobalKey _secondButtonKey = GlobalKey(); + final GlobalKey _thirdSlideKey = GlobalKey(); + final GlobalKey _fourthSlideKey = GlobalKey(); + final GlobalKey _fifthSlideKey = GlobalKey(); + final GlobalKey _sixthSlideKey = GlobalKey(); + final GlobalKey _seventhSlideKey = GlobalKey(); @override Widget build(BuildContext context) { @@ -44,18 +48,26 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { _secondSlide(textStyle), _thirdSlide(textStyle), _fourthSlide(textStyle), + _fifthSlide(textStyle), + _sixthSlide(textStyle), + _seventhSlide(textStyle), + _absoluteSlide(textStyle), ], child: _BubbleShowcaseDemoChild( - _titleKey, - _firstButtonKey, - _secondButtonKey, + _firstSlideKey, + _secondSlideKey, + _thirdSlideKey, + _fourthSlideKey, + _fifthSlideKey, + _sixthSlideKey, + _seventhSlideKey, ), ); } /// Creates the first slide. BubbleSlide _firstSlide(TextStyle textStyle) => RelativeBubbleSlide( - widgetKey: _titleKey, + widgetKey: _firstSlideKey, child: RelativeBubbleSlideChild( widget: Padding( padding: const EdgeInsets.only(top: 8), @@ -69,14 +81,14 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - 'That\'s cool !', + 'Hello World!', style: textStyle.copyWith( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( - 'This is my brand new title !', + 'BubbleShowcase lets you create step by step showcase of your features', style: textStyle, ), ], @@ -88,7 +100,7 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ); /// Creates the second slide. - BubbleSlide _secondSlide(TextStyle textStyle) => AbsoluteBubbleSlide( + BubbleSlide _absoluteSlide(TextStyle textStyle) => AbsoluteBubbleSlide( positionCalculator: (size) => Position( top: 0, right: 0, @@ -102,9 +114,9 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { nipLocation: NipLocation.LEFT, color: Colors.teal, child: Padding( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.all(20), child: Text( - 'Look at me pointing absolutely nothing.\n(Or maybe that\'s an hidden navigation bar !)', + 'Look at me pointing absolutely nothing.\n(Or maybe that\'s a hidden navigation bar!)', style: textStyle, ), ), @@ -115,34 +127,31 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ); /// Creates the third slide. - BubbleSlide _thirdSlide(TextStyle textStyle) => RelativeBubbleSlide( - widgetKey: _firstButtonKey, - shape: const Oval( - spreadRadius: 15, - ), + BubbleSlide _secondSlide(TextStyle textStyle) => RelativeBubbleSlide( + widgetKey: _secondSlideKey, child: RelativeBubbleSlideChild( + direction: AxisDirection.down, widget: Padding( - padding: const EdgeInsets.only(top: 23), + padding: const EdgeInsets.only(top: 8.0), child: SpeechBubble( nipLocation: NipLocation.TOP, - color: Colors.purple, + color: Colors.blue, child: Padding( padding: const EdgeInsets.all(10), - child: Row( + child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Padding( - padding: EdgeInsets.only(right: 5), - child: Icon( - Icons.info_outline, - color: Colors.white, + Text( + 'Second slide!', + style: textStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, ), ), - Expanded( - child: Text( - 'As said, this button is new.\nOh, and this one is oval by the way.', - style: textStyle, - ), + const SizedBox(height: 10), + Text( + 'This slide uses the default positioning which will center the container\'s content within the dimensions of the highlighted box.', + style: textStyle, ), ], ), @@ -153,18 +162,17 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ); /// Creates the fourth slide. - BubbleSlide _fourthSlide(TextStyle textStyle) => RelativeBubbleSlide( + BubbleSlide _thirdSlide(TextStyle textStyle) => RelativeBubbleSlide( highlightPadding: 4, passThroughMode: PassthroughMode.INSIDE_WITH_NOTIFICATION, - widgetKey: _secondButtonKey, - shape: const Oval( - spreadRadius: 15, - ), + widgetKey: _thirdSlideKey, child: RelativeBubbleSlideChild( + enableExtraSpace: true, + direction: AxisDirection.down, widget: Padding( - padding: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.all(8.0), child: SpeechBubble( - nipLocation: NipLocation.TOP, + nipLocation: NipLocation.TOP_LEFT, color: Colors.blue, child: Padding( padding: const EdgeInsets.all(10), @@ -173,7 +181,7 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - 'Going through!', + 'Click me to continue!', style: textStyle.copyWith( fontSize: 18, fontWeight: FontWeight.bold, @@ -181,7 +189,7 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ), const SizedBox(height: 10), Text( - 'Passthrough is on!\nTo finish the tutorial, you need to click this button', + 'This slide is on the top left with `enableExtraSpace = true`\nWhen this is enabled it will automatically expand to the side with the most space, to expand further than the highlighted area\'s dimentions.\nThere is also some highlight padding on this one.\n\nAlso passthrough mode is on so you can now interact with the button.\nTo continue the tutorial, you need to click this button.', style: textStyle, ), ], @@ -191,20 +199,220 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ), ), ); + + BubbleSlide _fourthSlide(TextStyle textStyle) => RelativeBubbleSlide( + widgetKey: _fourthSlideKey, + child: RelativeBubbleSlideChild( + enableExtraSpace: true, + direction: AxisDirection.down, + widget: Padding( + padding: const EdgeInsets.all(8.0), + child: SpeechBubble( + color: Colors.blue, + nipLocation: NipLocation.TOP_RIGHT, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Fourth Slide!', + style: textStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(right: 5), + child: Icon( + Icons.info_outline, + color: Colors.white, + ), + ), + Text( + 'Another example of the automatic resizing.\n\nThis one is on the top right and it will expand to the bottom left if needed!\n\nNote that the positioning is assisted by an `Alignment.topRight`', + style: textStyle, + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + + BubbleSlide _fifthSlide(TextStyle textStyle) => RelativeBubbleSlide( + widgetKey: _fifthSlideKey, + child: RelativeBubbleSlideChild( + enableExtraSpace: true, + direction: AxisDirection.up, + widget: Padding( + padding: const EdgeInsets.all(8.0), + child: SpeechBubble( + nipLocation: NipLocation.BOTTOM_RIGHT, + color: Colors.purple, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Fifth Slide!', + style: textStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(right: 5), + child: Icon( + Icons.info_outline, + color: Colors.white, + ), + ), + Text( + 'Another example of the automatic resizing.\n\nThis is one is on bottom right and it will expand to the top left if needed!\n Note the MainAxisSize.min on both the column and row to shrinkwrap the content', + style: textStyle, + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + + BubbleSlide _sixthSlide(TextStyle textStyle) => RelativeBubbleSlide( + widgetKey: _sixthSlideKey, + shape: const Oval( + spreadRadius: 15, + ), + child: RelativeBubbleSlideChild( + enableExtraSpace: true, + direction: AxisDirection.up, + widget: Padding( + padding: const EdgeInsets.all(16.0), + child: SpeechBubble( + nipLocation: NipLocation.BOTTOM_LEFT, + color: Colors.purple, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Sixth slide!', + style: textStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(right: 5), + child: Icon( + Icons.info_outline, + color: Colors.white, + ), + ), + Text( + 'Another example of the automatic resizing.\n\nOh, and this one is oval by the way.', + style: textStyle, + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + + BubbleSlide _seventhSlide(TextStyle textStyle) => RelativeBubbleSlide( + widgetKey: _seventhSlideKey, + child: RelativeBubbleSlideChild( + enableExtraSpace: true, + direction: AxisDirection.left, + widget: Padding( + padding: const EdgeInsets.all(8.0), + child: SpeechBubble( + color: Colors.blue, + nipLocation: NipLocation.RIGHT, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Center positioned!', + style: textStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(right: 5), + child: Icon( + Icons.info_outline, + color: Colors.white, + ), + ), + Flexible( + child: Text( + 'Another example of the automatic resizing. This one will try to expand to the left, top and bottom, it still is limited vertically, but it is bigger than its highlighted area', + style: textStyle, + ), + ) + ], + ), + ], + ), + ), + ), + ), + ), + ); } /// The main demo widget child. class _BubbleShowcaseDemoChild extends StatelessWidget { /// The title text global key. - final GlobalKey _titleKey; + final GlobalKey _firstSlideKey; /// The first button global key. - final GlobalKey _firstButtonKey; - final GlobalKey _secondButtonKey; + final GlobalKey _secondSlideKey; + final GlobalKey _thirdSlideKey; + final GlobalKey _fourthSlideKey; + final GlobalKey _fifthSlideKey; + final GlobalKey _sixthSlideKey; + final GlobalKey _seventhSlideKey; /// Creates a new bubble showcase demo child instance. _BubbleShowcaseDemoChild( - this._titleKey, this._firstButtonKey, this._secondButtonKey); + this._firstSlideKey, + this._secondSlideKey, + this._thirdSlideKey, + this._fourthSlideKey, + this._fifthSlideKey, + this._sixthSlideKey, + this._seventhSlideKey, + ); @override Widget build(BuildContext context) => Padding( @@ -219,7 +427,7 @@ class _BubbleShowcaseDemoChild extends StatelessWidget { width: MediaQuery.of(context).size.width, child: Text( 'Bubble Showcase', - key: _titleKey, + key: _firstSlideKey, style: Theme.of(context).textTheme.headline4, textAlign: TextAlign.center, ), @@ -227,19 +435,59 @@ class _BubbleShowcaseDemoChild extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 30, bottom: 5), child: ElevatedButton( - key: _firstButtonKey, + key: _secondSlideKey, onPressed: () {}, child: const Text('This button is NEW !'), ), ), - ElevatedButton( - key: _secondButtonKey, - onPressed: () { - const BubbleShowcaseNotification()..dispatch(context); - }, - child: const Text( - 'This button is old, please don\'t pay attention.', - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + key: _thirdSlideKey, + onPressed: () { + const BubbleShowcaseNotification()..dispatch(context); + }, + child: const Text( + 'This button is to the left', + ), + ), + ElevatedButton( + key: _fourthSlideKey, + onPressed: () {}, + child: const Text( + 'This button is to the right', + ), + ), + ], + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + key: _seventhSlideKey, + onPressed: () {}, + child: const Text( + 'This button is on the center', + ), + ), + ], + ), + const Spacer(), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + key: _sixthSlideKey, + child: const Text('This text is to the left'), + ), + Container( + key: _fifthSlideKey, + child: const Text('This text is to the right'), + ) + ], ) ], ), From 768f3c2b76b5bbc6d397c838a23832fccc92dbea Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Thu, 14 Oct 2021 16:51:50 -0600 Subject: [PATCH 03/16] style: cleanup prints --- lib/src/slide.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 2e9ca25..25035f3 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -387,7 +387,6 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { // It will expand to the left final spacingFromTheRightEdge = parentSize.width - highlightPosition.right; - print(highlightPosition); return Position( right: spacingFromTheRightEdge, top: highlightPosition.bottom, @@ -518,7 +517,6 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { // It will expand to the left final spacingFromTheRightEdge = parentSize.width - highlightPosition.right; - print(highlightPosition); return Position( right: spacingFromTheRightEdge, bottom: parentSize.height - highlightPosition.top, From 0c5d3868e3d0009b33bf18653b289422780ecc2e Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Mon, 18 Oct 2021 10:47:34 -0600 Subject: [PATCH 04/16] feat: fix slide to center on horizontal axis slide --- lib/src/slide.dart | 68 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 25035f3..049c378 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -415,7 +415,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { : availableWidth / 2, ); default: - throw ("Positioning is not optimal"); + throw ("Slide is outside the view area"); } } @@ -445,16 +445,32 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { final bottomHeightFromEdge = parentSize.height - highlightPosition.bottom; final availableHeight = topHeightFromEdge > bottomHeightFromEdge - ? parentSize.height - highlightPosition.bottom - : parentSize.height - highlightPosition.top; + ? (parentSize.height - highlightPosition.bottom) - + (parentSize.height - highlightPosition.top) + : (parentSize.height - highlightPosition.top) - + (parentSize.height - highlightPosition.bottom); + final highlightedItemSize = Size( + highlightPosition.right - highlightPosition.left, + highlightPosition.bottom - highlightPosition.top); + double top; + double bottom; + if (topHeightFromEdge > bottomHeightFromEdge) { + top = (topHeightFromEdge) - + ((availableHeight / 2) + + (bottomHeightFromEdge / 2) + + highlightedItemSize.height); + bottom = (bottomHeightFromEdge / 2) - highlightedItemSize.height / 2; + } else { + top = (topHeightFromEdge / 2) - highlightedItemSize.height; + bottom = (bottomHeightFromEdge) - + ((availableHeight / 2) + + (topHeightFromEdge / 2) + + highlightedItemSize.height / 2); + } return Position( - top: topHeightFromEdge > bottomHeightFromEdge - ? (topHeightFromEdge) - (availableHeight / 2) - : 0, - bottom: bottomHeightFromEdge > topHeightFromEdge - ? (bottomHeightFromEdge) + (availableHeight / 2) - : availableHeight / 2, + top: top, + bottom: bottom, right: highlightPosition.right, ); @@ -488,17 +504,35 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { final topHeightFromEdge = highlightPosition.top; final bottomHeightFromEdge = parentSize.height - highlightPosition.bottom; + final availableHeight = topHeightFromEdge > bottomHeightFromEdge - ? parentSize.height - highlightPosition.bottom - : parentSize.height - highlightPosition.top; + ? (parentSize.height - highlightPosition.bottom) - + (parentSize.height - highlightPosition.top) + : (parentSize.height - highlightPosition.top) - + (parentSize.height - highlightPosition.bottom); + final highlightedItemSize = Size( + highlightPosition.right - highlightPosition.left, + highlightPosition.bottom - highlightPosition.top, + ); + double top; + double bottom; + if (topHeightFromEdge > bottomHeightFromEdge) { + top = (topHeightFromEdge) - + ((availableHeight / 2) + + (bottomHeightFromEdge / 2) + + highlightedItemSize.height); + bottom = (bottomHeightFromEdge / 2) - highlightedItemSize.height / 2; + } else { + top = (topHeightFromEdge / 2) - highlightedItemSize.height; + bottom = (bottomHeightFromEdge) - + ((availableHeight / 2) + + (topHeightFromEdge / 2) + + highlightedItemSize.height / 2); + } return Position( - top: topHeightFromEdge > bottomHeightFromEdge - ? (topHeightFromEdge) - (availableHeight / 2) - : 0, - bottom: bottomHeightFromEdge > topHeightFromEdge - ? (bottomHeightFromEdge) + (availableHeight / 2) - : availableHeight / 2, + top: top, + bottom: bottom, left: highlightPosition.right, ); default: From 78f4dbd5079020e169a0234ab5d56d10c2d111fc Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Tue, 19 Oct 2021 01:30:06 -0600 Subject: [PATCH 05/16] refactor: improve automatic positioning --- lib/src/slide.dart | 106 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 14 deletions(-) diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 049c378..57f7a56 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -2,6 +2,7 @@ import 'package:bubble_showcase/src/shape.dart'; import 'package:bubble_showcase/src/showcase.dart'; import 'package:bubble_showcase/src/utils.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// A function that allows to calculate a position according to a provided size. @@ -257,7 +258,10 @@ abstract class BubbleSlideChild { right: position.right, bottom: position.bottom, left: position.left, - child: widget, + child: Container( + color: Colors.black, + child: widget, + ), ); } @@ -280,12 +284,30 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { /// Heavily assisted by using an `Align` widget to align it within the expanded space final bool enableExtraSpace; + /// Determines, in size a percentage from 0.5 to 0.95, the height of the parent container that will be + /// recognized as "Middle" space, starting from the center. + /// + /// Used by the automatic positioning system to determine + /// which positioning strategy to use. Defaults to 65%. + final double middlePointHeight; + + /// Determines, in size a percentage from 0.5 to 0.95, the width of the parent container that will be + /// recognized as "Middle" space, starting from the center. + /// + /// Used by the automatic positioning system to determine + /// which positioning strategy to use.Defaults to 65%. + final double middlePointWidth; + /// Creates a new relative bubble slide child instance. const RelativeBubbleSlideChild({ required Widget widget, this.direction = AxisDirection.down, this.enableExtraSpace = false, - }) : super( + this.middlePointWidth = 0.15, + this.middlePointHeight = 0.15, + }) : assert(middlePointHeight >= 0 && middlePointHeight < 0.45), + assert(middlePointWidth >= 0 && middlePointWidth < 0.45), + super( widget: widget, ); @@ -296,8 +318,8 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { Size parentSize, ) { if (enableExtraSpace) { - int quadrant = - _getQuadrantFromRelativePosition(highlightPosition, parentSize); + int quadrant = _getQuadrantFromRelativePosition( + highlightPosition, parentSize, direction); switch (direction) { case AxisDirection.up: @@ -355,21 +377,76 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { int _getQuadrantFromRelativePosition( Position highlightPosition, Size parentSize, + AxisDirection direction, ) { final r = highlightPosition.right; final l = highlightPosition.left; final t = highlightPosition.top; final b = highlightPosition.bottom; - final p = parentSize; + final w = parentSize.width; + final h = parentSize.height; + // Represents the boundaries x1 & y1 represent the positive axes from the middle of the parent + // While x2 & y2 represent the negative axes from the middle of the parent + + final middlePointWidthConverted = 0.5 + middlePointWidth; + final middlePointHeightConverted = 0.5 + middlePointHeight; + + final x1 = w * middlePointWidthConverted; + final x2 = w - (w * middlePointWidthConverted); + + final y1 = h - (h * middlePointHeightConverted); + final y2 = h * middlePointHeightConverted; + + // Mx & My represent the middle points of the axes of the highlighted item + final highlightAreaSize = Size(r - l, b - t); + final mx = l + highlightAreaSize.width / 2; + final my = t + highlightAreaSize.height / 2; + + final leansToRightSide = highlightPosition.right < highlightPosition.left; + final leansToBottomSide = highlightPosition.bottom < highlightPosition.top; + + final isHorizontal = + direction == AxisDirection.left || direction == AxisDirection.right; + + final isVertical = + direction == AxisDirection.up || direction == AxisDirection.down; - if (l >= p.width / 2 && b <= p.height / 2) { + // Calculate extremes first, cases where we cannot center (AKA quadrant 5) + if (r >= w * 0.95 && isVertical) { + if (leansToBottomSide) { + return 4; + } else { + return 1; + } + } else if (l <= w * 0.05 && isVertical) { + if (leansToBottomSide) { + return 3; + } else { + return 2; + } + } else if (b >= h * 0.95 && isHorizontal) { + if (leansToRightSide) { + return 4; + } else { + return 3; + } + } else if (t <= h * 0.05 && isHorizontal) { + if (leansToRightSide) { + return 1; + } else { + return 2; + } + } + + // Calculate quadrants normally + if (mx >= x1 && my <= y1) { return 1; // top right - } else if (r <= p.width / 2 && b <= p.height / 2) { + } else if (mx <= x2 && my <= y1) { return 2; // top left - } else if (r <= p.width / 2 && t >= p.height / 2) { + } else if (mx <= x2 && my >= y2) { return 3; // bottom right - } else if (l >= p.width / 2 && t >= p.height / 2) { + } else if (mx >= x1 && my >= y2) { return 4; // bottom left } else { return 5; // center (Not totally within any other quadrant) @@ -436,7 +513,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { case 4: // It will expand to the top return Position( - bottom: parentSize.height - highlightPosition.top, + bottom: parentSize.height - highlightPosition.bottom, right: parentSize.width - highlightPosition.left, ); case 5: @@ -450,8 +527,9 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { : (parentSize.height - highlightPosition.top) - (parentSize.height - highlightPosition.bottom); final highlightedItemSize = Size( - highlightPosition.right - highlightPosition.left, - highlightPosition.bottom - highlightPosition.top); + highlightPosition.right - highlightPosition.left, + highlightPosition.bottom - highlightPosition.top, + ); double top; double bottom; if (topHeightFromEdge > bottomHeightFromEdge) { @@ -471,7 +549,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { return Position( top: top, bottom: bottom, - right: highlightPosition.right, + right: parentSize.width - highlightPosition.left, ); default: @@ -496,7 +574,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { case 4: // It will expand to the top return Position( - bottom: parentSize.height - highlightPosition.top, + bottom: parentSize.height - highlightPosition.bottom, left: highlightPosition.right, ); case 5: From ae7644995ef27d5563822970fa8b0e311bca561c Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Tue, 19 Oct 2021 01:38:13 -0600 Subject: [PATCH 06/16] refactor: remove black debug container --- lib/src/slide.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 57f7a56..b86b19c 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -258,10 +258,7 @@ abstract class BubbleSlideChild { right: position.right, bottom: position.bottom, left: position.left, - child: Container( - color: Colors.black, - child: widget, - ), + child: widget, ); } From b1aaf5fdfd3ecd8a00a50604e78ba631f15df16c Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Tue, 19 Oct 2021 10:46:06 -0600 Subject: [PATCH 07/16] refactor: enable onEnter/onExit properties --- example/lib/main.dart | 6 ++++++ lib/src/showcase.dart | 5 +++-- lib/src/slide.dart | 28 ++++++++++++++++++++++------ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a1462c9..6767a99 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -67,6 +67,12 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { /// Creates the first slide. BubbleSlide _firstSlide(TextStyle textStyle) => RelativeBubbleSlide( + onEnter: () { + print("OnEnter function!"); + }, + onExit: () { + print("OnExit function!"); + }, widgetKey: _firstSlideKey, child: RelativeBubbleSlideChild( widget: Padding( diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index 401452f..9facfa0 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -171,9 +171,10 @@ class _BubbleShowcaseState extends State /// Allows to trigger exit callbacks. void triggerOnExit() { - if (currentSlideIndex >= 0 && + if (currentSlideIndex > 0 && currentSlideIndex < widget.bubbleSlides.length) { - VoidCallback? callback = widget.bubbleSlides[currentSlideIndex].onExit; + VoidCallback? callback = + widget.bubbleSlides[currentSlideIndex - 1].onExit; if (callback != null) { callback(); } diff --git a/lib/src/slide.dart b/lib/src/slide.dart index b86b19c..76273df 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -34,7 +34,7 @@ abstract class BubbleSlide { /// Triggered when this slide has been exited. final VoidCallback? onExit; - final PassthroughMode passthroughMode; + final PassthroughMode passThroughMode; /// The slide child. final BubbleSlideChild? child; @@ -50,7 +50,7 @@ abstract class BubbleSlide { this.onEnter, this.onExit, this.child, - this.passthroughMode = PassthroughMode.NONE, + this.passThroughMode = PassthroughMode.NONE, }); /// Builds the whole slide widget. @@ -68,7 +68,7 @@ abstract class BubbleSlide { List children; - switch (passthroughMode) { + switch (passThroughMode) { case PassthroughMode.NONE: children = [ Positioned.fill( @@ -142,7 +142,7 @@ abstract class BubbleSlide { )); } - if (passthroughMode == PassthroughMode.INSIDE_WITH_NOTIFICATION) { + if (passThroughMode == PassthroughMode.INSIDE_WITH_NOTIFICATION) { return Stack( children: children, ); @@ -172,6 +172,11 @@ class RelativeBubbleSlide extends BubbleSlide { /// Padding for the highlight area final int highlightPadding; + final PassthroughMode passThroughMode; + + final VoidCallback? onEnter; + final VoidCallback? onExit; + /// Creates a new relative bubble slide instance. const RelativeBubbleSlide({ Shape shape = const Rectangle(), @@ -180,15 +185,19 @@ class RelativeBubbleSlide extends BubbleSlide { blurRadius: 0, spreadRadius: 0, ), - passThroughMode = PassthroughMode.NONE, required BubbleSlideChild child, required this.widgetKey, + this.passThroughMode = PassthroughMode.NONE, this.highlightPadding = 0, + this.onEnter, + this.onExit, }) : super( shape: shape, boxShadow: boxShadow, child: child, - passthroughMode: passThroughMode, + passThroughMode: passThroughMode, + onEnter: onEnter, + onExit: onExit, ); @override @@ -215,6 +224,9 @@ class AbsoluteBubbleSlide extends BubbleSlide { /// The function that allows to compute the highlight position according to the parent size. final PositionCalculator positionCalculator; + final VoidCallback? onEnter; + final VoidCallback? onExit; + /// Creates a new absolute bubble slide instance. const AbsoluteBubbleSlide({ Shape shape = const Rectangle(), @@ -225,10 +237,14 @@ class AbsoluteBubbleSlide extends BubbleSlide { ), required BubbleSlideChild child, required this.positionCalculator, + this.onEnter, + this.onExit, }) : super( shape: shape, boxShadow: boxShadow, child: child, + onEnter: onEnter, + onExit: onExit, ); @override From 92ae1915b7a5b88e3861e17b80cc7dea03c0cba5 Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Tue, 19 Oct 2021 13:25:57 -0600 Subject: [PATCH 08/16] fix: onExit not executing on last slide --- example/lib/main.dart | 6 ++++++ lib/src/showcase.dart | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6767a99..5d8e439 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -107,6 +107,12 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { /// Creates the second slide. BubbleSlide _absoluteSlide(TextStyle textStyle) => AbsoluteBubbleSlide( + onEnter: () { + print("OnEnter function!"); + }, + onExit: () { + print("OnExit function!"); + }, positionCalculator: (size) => Position( top: 0, right: 0, diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index 9facfa0..5027f94 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -172,7 +172,7 @@ class _BubbleShowcaseState extends State /// Allows to trigger exit callbacks. void triggerOnExit() { if (currentSlideIndex > 0 && - currentSlideIndex < widget.bubbleSlides.length) { + currentSlideIndex <= widget.bubbleSlides.length) { VoidCallback? callback = widget.bubbleSlides[currentSlideIndex - 1].onExit; if (callback != null) { From 5d37a7f65c8591387f04aa4f4c7980101ec2d31e Mon Sep 17 00:00:00 2001 From: Mario Date: Thu, 21 Oct 2021 12:56:42 -0600 Subject: [PATCH 09/16] feat: add onDismiss and onEnd --- example/lib/main.dart | 20 +++++++++++++------- lib/src/showcase.dart | 13 +++++++++++++ lib/src/slide.dart | 21 ++++++++++++++------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5d8e439..1177534 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -41,6 +41,12 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { color: Colors.white, ); return BubbleShowcase( + onDismiss: () { + print('I got dismissed!'); + }, + onEnd: () { + print('Tutorial has ended!'); + }, bubbleShowcaseId: 'my_bubble_showcase', bubbleShowcaseVersion: 1, bubbleSlides: [ @@ -68,10 +74,10 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { /// Creates the first slide. BubbleSlide _firstSlide(TextStyle textStyle) => RelativeBubbleSlide( onEnter: () { - print("OnEnter function!"); + print('OnEnter function!'); }, onExit: () { - print("OnExit function!"); + print('OnExit function!'); }, widgetKey: _firstSlideKey, child: RelativeBubbleSlideChild( @@ -105,13 +111,13 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ), ); - /// Creates the second slide. + /// Creates the absolute slide. BubbleSlide _absoluteSlide(TextStyle textStyle) => AbsoluteBubbleSlide( onEnter: () { - print("OnEnter function!"); + print('OnEnter function!'); }, onExit: () { - print("OnExit function!"); + print('OnExit function!'); }, positionCalculator: (size) => Position( top: 0, @@ -138,7 +144,7 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ), ); - /// Creates the third slide. + /// Creates the second slide. BubbleSlide _secondSlide(TextStyle textStyle) => RelativeBubbleSlide( widgetKey: _secondSlideKey, child: RelativeBubbleSlideChild( @@ -173,7 +179,7 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { ), ); - /// Creates the fourth slide. + /// Creates the third slide. BubbleSlide _thirdSlide(TextStyle textStyle) => RelativeBubbleSlide( highlightPadding: 4, passThroughMode: PassthroughMode.INSIDE_WITH_NOTIFICATION, diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index 5027f94..25e013f 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -34,6 +34,14 @@ class BubbleShowcase extends StatefulWidget { /// Wether this showcase should be presented. final bool enabled; + /// Handler that executes when the showcase is dismissed by the close icon + final VoidCallback? onDismiss; + + /// Handler that will execute when the showcase finishes + /// + /// Note that this handler will also execute when `onDismiss` is triggered + final VoidCallback? onEnd; + /// Creates a new bubble showcase instance. BubbleShowcase({ required this.bubbleShowcaseId, @@ -45,6 +53,8 @@ class BubbleShowcase extends StatefulWidget { this.showCloseButton = true, this.initialDelay = Duration.zero, this.enabled = true, + this.onDismiss, + this.onEnd, }) : assert(bubbleSlides.isNotEmpty); @override @@ -131,6 +141,9 @@ class _BubbleShowcaseState extends State triggerOnExit(); if (isFinished) { + if (widget.onEnd != null) { + widget.onEnd!(); + } currentSlideEntry = null; if (widget.doNotReopenOnClose) { SharedPreferences.getInstance().then((preferences) { diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 76273df..b2cbc1c 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -32,6 +32,8 @@ abstract class BubbleSlide { final VoidCallback? onEnter; /// Triggered when this slide has been exited. + /// + /// Also triggered when onDismissed is called on the BubbleShowcase final VoidCallback? onExit; final PassthroughMode passThroughMode; @@ -110,7 +112,7 @@ abstract class BubbleSlide { if (bubbleShowcase.counterText != null) { children.add( Positioned( - bottom: 5, + bottom: MediaQuery.of(context).padding.bottom + 5, left: 0, right: 0, child: Text( @@ -130,10 +132,15 @@ abstract class BubbleSlide { // Add Close button if (bubbleShowcase.showCloseButton) { children.add(Positioned( - top: MediaQuery.of(context).padding.top, + top: MediaQuery.of(context).padding.top + 5, left: 0, child: GestureDetector( - onTap: () => goToSlide(slidesCount), + onTap: () { + if (bubbleShowcase.onDismiss != null) { + bubbleShowcase.onDismiss!(); + } + goToSlide(slidesCount); + }, child: Icon( Icons.close, color: writeColor, @@ -505,7 +512,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { : availableWidth / 2, ); default: - throw ("Slide is outside the view area"); + throw ('Slide is outside the view area'); } } @@ -566,7 +573,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { ); default: - throw ("Slide is outside the view area"); + throw ('Slide is outside the view area'); } } @@ -627,7 +634,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { left: highlightPosition.right, ); default: - throw ("Slide is outside the view area"); + throw ('Slide is outside the view area'); } } @@ -670,7 +677,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { : availableWidth / 2, ); default: - throw ("Slide is outside the view area"); + throw ('Slide is outside the view area'); } } } From 3aa60bb851d837abc7097ade8630a29ceba4caa2 Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Fri, 22 Oct 2021 09:33:29 -0600 Subject: [PATCH 10/16] refactor: separate onEnd from onDismiss --- lib/src/showcase.dart | 14 ++++++++++++++ lib/src/slide.dart | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index 25e013f..8cf2324 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -159,6 +159,19 @@ class _BubbleShowcaseState extends State } } + void close() { + currentSlideEntry?.remove(); + triggerOnExit(); + currentSlideEntry = null; + if (widget.doNotReopenOnClose) { + SharedPreferences.getInstance().then((preferences) { + preferences.setBool( + '${widget.bubbleShowcaseId}.${widget.bubbleShowcaseVersion}', + false); + }); + } + } + /// Creates the current slide entry. OverlayEntry createCurrentSlideEntry() => OverlayEntry( builder: (context) => widget.bubbleSlides[currentSlideIndex].build( @@ -168,6 +181,7 @@ class _BubbleShowcaseState extends State (position) { setState(() => goToNextEntryOrClose(position)); }, + close, ), ); diff --git a/lib/src/slide.dart b/lib/src/slide.dart index b2cbc1c..5e7138c 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -61,6 +61,7 @@ abstract class BubbleSlide { BubbleShowcase bubbleShowcase, int currentSlideIndex, void Function(int) goToSlide, + VoidCallback close, ) { Position highlightPosition = getHighlightPosition( context, @@ -139,7 +140,7 @@ abstract class BubbleSlide { if (bubbleShowcase.onDismiss != null) { bubbleShowcase.onDismiss!(); } - goToSlide(slidesCount); + close(); }, child: Icon( Icons.close, From 572c3af8ae604fa0505187122686197f8fe74c82 Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 14 Nov 2021 19:32:22 -0600 Subject: [PATCH 11/16] wip: add advanced positioning demo --- example/lib/draggableShowcase.dart | 251 +++++++++++++++++++++++++++++ example/lib/main.dart | 24 ++- 2 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 example/lib/draggableShowcase.dart diff --git a/example/lib/draggableShowcase.dart b/example/lib/draggableShowcase.dart new file mode 100644 index 0000000..259a9ab --- /dev/null +++ b/example/lib/draggableShowcase.dart @@ -0,0 +1,251 @@ +import 'package:bubble_showcase/bubble_showcase.dart'; +import 'package:flutter/material.dart'; + +import 'speech_bubble.dart'; + +final middlePointHeight = 0.20; +final middlePointWidth = 0.20; + +final buttonHeight = 80.00; +final buttonWidth = 220.00; + +/// The draggable demo widget +class BubbleShowcaseDraggableWidget extends StatefulWidget { + @override + BubbleShowcaseDraggableWidgetState createState() => + BubbleShowcaseDraggableWidgetState(); +} + +class BubbleShowcaseDraggableWidgetState + extends State { + int tapAmount = 0; + bool enabled = false; + final GlobalKey _draggableSlideKey = GlobalKey(); + + callback() { + setState(() { + enabled = !enabled; + }); + } + + @override + Widget build(BuildContext context) { + TextStyle textStyle = Theme.of(context).textTheme.bodyText2!.copyWith( + color: Colors.white, + ); + return BubbleShowcase( + enabled: enabled, + initialDelay: const Duration(milliseconds: 500), + onDismiss: () { + print('I got dismissed!'); + }, + onEnd: () { + callback(); + }, + bubbleShowcaseId: 'my_bubble_showcase_2', + bubbleShowcaseVersion: 1, + bubbleSlides: [ + _draggableSlide(textStyle), + ], + child: _BubbleShowcaseDraggableChild( + _draggableSlideKey, + callback, + enabled, + ), + ); + } + + BubbleSlide _draggableSlide(TextStyle textStyle) => RelativeBubbleSlide( + widgetKey: _draggableSlideKey, + child: RelativeBubbleSlideChild( + middlePointHeight: middlePointHeight, + middlePointWidth: middlePointWidth, + enableExtraSpace: true, + direction: AxisDirection.down, + widget: Padding( + padding: const EdgeInsets.all(8.0), + child: SpeechBubble( + color: Colors.blue, + nipLocation: NipLocation.TOP_RIGHT, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Example Slide', + style: textStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(right: 5), + child: Icon( + Icons.info_outline, + color: Colors.white, + ), + ), + Flexible( + child: Text( + 'Example of advanced positioning system.', + style: textStyle, + ), + ) + ], + ), + ], + ), + ), + ), + ), + ), + ); +} + +class _BubbleShowcaseDraggableChild extends StatefulWidget { + /// The first button global key. + final GlobalKey _draggableSlideKey; + final VoidCallback callback; + final bool enabled; + + /// Creates a new bubble showcase demo child instance. + _BubbleShowcaseDraggableChild( + this._draggableSlideKey, + this.callback, + this.enabled, + ); + + @override + _BubbleShowcaseDraggableChildState createState() => + _BubbleShowcaseDraggableChildState(); +} + +class _BubbleShowcaseDraggableChildState + extends State<_BubbleShowcaseDraggableChild> { + Offset? _position; + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final debugLines = [ + // Extreme zone Left (Vertical) + Positioned( + left: MediaQuery.of(context).size.width * 0.05, + top: 0, + bottom: MediaQuery.of(context).size.height * 0.055, + child: const VerticalDivider( + thickness: 5, + color: Colors.red, + ), + ), + // Extreme zone Right (Vertical) + Positioned( + right: MediaQuery.of(context).size.width * 0.05, + top: 0, + bottom: MediaQuery.of(context).size.height * 0.055, + child: const VerticalDivider( + thickness: 5, + color: Colors.red, + ), + ), + // Extreme zone Bottom (Horizontal) + Positioned( + bottom: MediaQuery.of(context).size.height * 0.05, + left: MediaQuery.of(context).size.width * 0.055, + right: MediaQuery.of(context).size.width * 0.055, + child: const Divider( + thickness: 5, + color: Colors.red, + ), + ), + // Center zone left (Vertical) + Positioned( + top: 0, + bottom: MediaQuery.of(context).size.height * 0.055, + right: MediaQuery.of(context).size.width * (middlePointWidth + 0.5), + child: const VerticalDivider( + color: Colors.blue, + thickness: 5, + ), + ), + // Center zone right (Vertical) + Positioned( + top: 0, + bottom: MediaQuery.of(context).size.height * 0.055, + left: MediaQuery.of(context).size.width * (middlePointWidth + 0.5), + child: const VerticalDivider( + color: Colors.blue, + thickness: 5, + ), + ), + // Center zone bottom (Horizontal) + Positioned( + bottom: MediaQuery.of(context).size.height * + (middlePointHeight + 0.5 - 1), + left: MediaQuery.of(context).size.width * 0.055, + right: MediaQuery.of(context).size.width * 0.055, + child: const Divider( + color: Colors.blue, + thickness: 5, + ), + ), + // Center zone top (Horizontal) + Positioned( + bottom: + MediaQuery.of(context).size.height * (middlePointHeight + 0.5), + left: MediaQuery.of(context).size.width * 0.055, + right: MediaQuery.of(context).size.width * 0.055, + child: const Divider( + color: Colors.blue, + thickness: 5, + ), + ) + ]; + + final button = Container( + width: buttonWidth, + height: buttonHeight, + child: ElevatedButton( + onPressed: () { + widget.callback(); + }, + child: const Text( + 'Drag this button to position it\nClick to see the showcase slide', + ), + ), + ); + return Stack( + children: [ + ...debugLines, + Positioned( + left: MediaQuery.of(context).size.width / 2 - 80, + child: Text('Tutorial enabled? ${widget.enabled}'), + ), + Positioned( + left: _position != null + ? _position!.dx + : constraints.maxWidth / 2 - buttonWidth / 2, + top: _position != null + ? _position!.dy + : constraints.maxHeight / 2 - buttonHeight / 2, + child: Draggable( + key: widget._draggableSlideKey, + feedback: button, + onDraggableCanceled: (velocity, offset) => { + setState(() { + _position = Offset(offset.dx, offset.dy - 104); + }) + }, + child: button, + ), + ), + ], + ); + }, + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 1177534..868d98d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:bubble_showcase/bubble_showcase.dart'; +import 'package:bubble_showcase_example/draggableShowcase.dart'; import 'package:flutter/material.dart'; import 'speech_bubble.dart'; @@ -11,11 +12,25 @@ class _BubbleShowcaseDemoApp extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( title: 'Bubble Showcase Demo', - home: Scaffold( - appBar: AppBar( - title: const Text('Bubble Showcase Demo'), + home: DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('Bubble Showcase Demo'), + bottom: const TabBar( + tabs: [ + const Tab(text: 'Advanced positioning demo'), + const Tab(text: 'Demo'), + ], + ), + ), + body: TabBarView( + children: [ + BubbleShowcaseDraggableWidget(), + _BubbleShowcaseDemoWidget(), + ], + ), ), - body: _BubbleShowcaseDemoWidget(), ), ); } @@ -41,6 +56,7 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { color: Colors.white, ); return BubbleShowcase( + initialDelay: const Duration(milliseconds: 500), onDismiss: () { print('I got dismissed!'); }, From 20533edf078fb2eccf930e01bc1e51b186d5412b Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 14 Nov 2021 19:33:39 -0600 Subject: [PATCH 12/16] fix: BubbleShowcase not starting when changing from enabled=false to enabled=true --- lib/src/showcase.dart | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index 25e013f..a1c5724 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -100,11 +100,28 @@ class _BubbleShowcaseState extends State } @override - Widget build(BuildContext context) => - NotificationListener( - onNotification: processNotification, - child: widget.child, - ); + void didUpdateWidget(BubbleShowcase oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.enabled != widget.enabled) { + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (await widget.shouldOpenShowcase) { + await Future.delayed(widget.initialDelay); + if (mounted) { + goToNextEntryOrClose(0); + } + } + }); + } + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: processNotification, + child: widget.child, + ); + } @override void dispose() { From 4a4f2c68f6b80e68a0a0f0b1916812a35c040b2b Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 14 Nov 2021 19:34:07 -0600 Subject: [PATCH 13/16] fix: quadrant calculation when extremely positioned --- lib/src/slide.dart | 58 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/src/slide.dart b/lib/src/slide.dart index b2cbc1c..3078d6f 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -304,18 +304,18 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { /// Heavily assisted by using an `Align` widget to align it within the expanded space final bool enableExtraSpace; - /// Determines, in size a percentage from 0.5 to 0.95, the height of the parent container that will be + /// Determines, in size a percentage from 0.15 to 0.45, the height of the parent container that will be /// recognized as "Middle" space, starting from the center. /// /// Used by the automatic positioning system to determine - /// which positioning strategy to use. Defaults to 65%. + /// which positioning strategy to use. Defaults to 15% of the area from the middle to be counted as "center space". final double middlePointHeight; - /// Determines, in size a percentage from 0.5 to 0.95, the width of the parent container that will be + /// Determines, in size a percentage from 0.15 to 0.45, the width of the parent container that will be /// recognized as "Middle" space, starting from the center. /// /// Used by the automatic positioning system to determine - /// which positioning strategy to use.Defaults to 65%. + /// which positioning strategy to use. Defaults to 15% of the area from the middle to be counted as "center space". final double middlePointWidth; /// Creates a new relative bubble slide child instance. @@ -341,6 +341,8 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { int quadrant = _getQuadrantFromRelativePosition( highlightPosition, parentSize, direction); + print('DEBUG => quadrant $quadrant'); + switch (direction) { case AxisDirection.up: return _getUpPositionFromQuadrant( @@ -399,11 +401,24 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { Size parentSize, AxisDirection direction, ) { + // Distance of the right point from the left edge final r = highlightPosition.right; + // Distance of the left point from the left edge final l = highlightPosition.left; + // Ditsance from the top point from the top final t = highlightPosition.top; + // Distance from the bottom point from the top final b = highlightPosition.bottom; + // Distance of the right point from the right edge + final dr = parentSize.width - r; + // Distance of the left point from the left edge + final dl = l; + // Distance of the top point from the top + final dt = t; + // Distance of the bottom point from the bottom; + final db = parentSize.height - b; + final w = parentSize.width; final h = parentSize.height; // Represents the boundaries x1 & y1 represent the positive axes from the middle of the parent @@ -423,8 +438,21 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { final mx = l + highlightAreaSize.width / 2; final my = t + highlightAreaSize.height / 2; - final leansToRightSide = highlightPosition.right < highlightPosition.left; - final leansToBottomSide = highlightPosition.bottom < highlightPosition.top; + // It leans to the right side if the distance from the right edge is less than from the right edge + final leansToRightSide = dr < dl; + + // It leans to the bottom side if the distance from the bottom edge is less than from the top edge + final leansToBottomSide = db < dt; + print('==================================='); + print('leansToBottomSide: $leansToBottomSide'); + print( + 'highlightPosition.bottom: ${highlightPosition.bottom}, highlightPosition.top: ${highlightPosition.top}', + ); + print('==================================='); + print('leansToRightSide: $leansToRightSide'); + print( + 'highlightPosition.right: ${highlightPosition.right}, highlightPosition.left: ${highlightPosition.left}', + ); final isHorizontal = direction == AxisDirection.left || direction == AxisDirection.right; @@ -433,25 +461,33 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { direction == AxisDirection.up || direction == AxisDirection.down; // Calculate extremes first, cases where we cannot center (AKA quadrant 5) - if (r >= w * 0.95 && isVertical) { + if (dr <= w * 0.05 && isVertical) { + // Extreme right (slide on top or bottom of highlighted area) + print('1st'); if (leansToBottomSide) { return 4; } else { return 1; } - } else if (l <= w * 0.05 && isVertical) { + } else if (dl <= w * 0.05 && isVertical) { + // Extreme left (slide on top or bottom of highlighted area) + print('2nd'); if (leansToBottomSide) { return 3; } else { return 2; } - } else if (b >= h * 0.95 && isHorizontal) { + } else if (db <= h * 0.05 && isHorizontal) { + // Extreme bottom (slide on left or right of highlighted area) + print('3rd'); if (leansToRightSide) { return 4; } else { return 3; } - } else if (t <= h * 0.05 && isHorizontal) { + } else if (dt <= h * 0.05 && isHorizontal) { + // Extreme top (slide on left or right of highlighted area) + print('4th'); if (leansToRightSide) { return 1; } else { @@ -459,6 +495,8 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { } } + print("Quadrant calculated normally"); + // Calculate quadrants normally if (mx >= x1 && my <= y1) { return 1; // top right From fc6e581a7752e3647556b1160502c2e6b28261c8 Mon Sep 17 00:00:00 2001 From: Mario Mejia Date: Sat, 4 Dec 2021 12:48:35 -0600 Subject: [PATCH 14/16] feat: use getAlignment for rendering relativeSlide --- lib/src/slide.dart | 141 ++++++++++++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 41 deletions(-) diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 669c32e..8a86bc0 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -20,6 +20,14 @@ enum PassthroughMode { NONE, } +enum Quadrant { + TOP_LEFT, + TOP_RIGHT, + CENTER, + BOTTOM_LEFT, + BOTTOM_RIGHT, +} + /// A simple bubble slide that allows to highlight a specific screen zone. abstract class BubbleSlide { /// The slide shape. @@ -277,12 +285,19 @@ abstract class BubbleSlideChild { /// Builds the bubble slide child widget. Widget build(BuildContext context, Position targetPosition, Size parentSize) { Position position = getPosition(context, targetPosition, parentSize); + Alignment alignment = getAlignment(context, targetPosition, parentSize); + + print("DEBUG => alignment: $alignment, position: $position"); + return Positioned( top: position.top, right: position.right, bottom: position.bottom, left: position.left, - child: widget, + child: Align( + alignment: alignment, + child: widget, + ), ); } @@ -292,6 +307,12 @@ abstract class BubbleSlideChild { Position highlightPosition, Size parentSize, ); + + Alignment getAlignment( + BuildContext context, + Position highlightPosition, + Size parentSize, + ); } /// A bubble slide with a position that depends on the highlight zone. @@ -339,7 +360,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { Size parentSize, ) { if (enableExtraSpace) { - int quadrant = _getQuadrantFromRelativePosition( + Quadrant quadrant = _getQuadrantFromRelativePosition( highlightPosition, parentSize, direction); print('DEBUG => quadrant $quadrant'); @@ -397,7 +418,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { } } - int _getQuadrantFromRelativePosition( + Quadrant _getQuadrantFromRelativePosition( Position highlightPosition, Size parentSize, AxisDirection direction, @@ -461,38 +482,40 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { final isVertical = direction == AxisDirection.up || direction == AxisDirection.down; - // Calculate extremes first, cases where we cannot center (AKA quadrant 5) + // Calculate extremes first, cases where + // it might be in quadrant 5, but we cannot center due to possible + // collitions with the screen edges. if (dr <= w * 0.05 && isVertical) { // Extreme right (slide on top or bottom of highlighted area) print('1st'); if (leansToBottomSide) { - return 4; + return Quadrant.BOTTOM_RIGHT; } else { - return 1; + return Quadrant.TOP_RIGHT; } } else if (dl <= w * 0.05 && isVertical) { // Extreme left (slide on top or bottom of highlighted area) print('2nd'); if (leansToBottomSide) { - return 3; + return Quadrant.BOTTOM_LEFT; } else { - return 2; + return Quadrant.TOP_LEFT; } } else if (db <= h * 0.05 && isHorizontal) { // Extreme bottom (slide on left or right of highlighted area) print('3rd'); if (leansToRightSide) { - return 4; + return Quadrant.BOTTOM_RIGHT; } else { - return 3; + return Quadrant.BOTTOM_LEFT; } } else if (dt <= h * 0.05 && isHorizontal) { // Extreme top (slide on left or right of highlighted area) print('4th'); if (leansToRightSide) { - return 1; + return Quadrant.TOP_RIGHT; } else { - return 2; + return Quadrant.TOP_LEFT; } } @@ -500,26 +523,30 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { // Calculate quadrants normally if (mx >= x1 && my <= y1) { - return 1; // top right + print("DEBUG => Top_right"); + return Quadrant.TOP_RIGHT; } else if (mx <= x2 && my <= y1) { - return 2; // top left + print("DEBUG => Top_left"); + return Quadrant.TOP_LEFT; } else if (mx <= x2 && my >= y2) { - return 3; // bottom right + print("DEBUG => Bottom_left"); + return Quadrant.BOTTOM_LEFT; } else if (mx >= x1 && my >= y2) { - return 4; // bottom left + print("DEBUG => Bottom_right"); + return Quadrant.BOTTOM_RIGHT; } else { - return 5; // center (Not totally within any other quadrant) + return Quadrant.CENTER; // center (Not totally within any other quadrant) } } Position _getDownPositionFromQuadrant( - int quadrant, + Quadrant quadrant, Size parentSize, Position highlightPosition, ) { switch (quadrant) { - case 1: - case 4: + case Quadrant.TOP_RIGHT: + case Quadrant.BOTTOM_RIGHT: // It will expand to the left final spacingFromTheRightEdge = parentSize.width - highlightPosition.right; @@ -527,14 +554,14 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { right: spacingFromTheRightEdge, top: highlightPosition.bottom, ); - case 2: - case 3: + case Quadrant.TOP_LEFT: + case Quadrant.BOTTOM_LEFT: // It will expand to the right return Position( left: highlightPosition.left, top: highlightPosition.bottom, ); - case 5: + case Quadrant.CENTER: final widthFromRightEdge = parentSize.width - highlightPosition.right; final widthFromLeftEdge = highlightPosition.left; final availableWidth = widthFromRightEdge > widthFromLeftEdge @@ -556,26 +583,26 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { } Position _getLeftPositionFromQuadrant( - int quadrant, + Quadrant quadrant, Size parentSize, Position highlightPosition, ) { switch (quadrant) { - case 1: - case 2: + case Quadrant.TOP_RIGHT: + case Quadrant.TOP_LEFT: // It will expand to the bottom return Position( top: highlightPosition.top, right: parentSize.width - highlightPosition.left, ); - case 3: - case 4: + case Quadrant.BOTTOM_RIGHT: + case Quadrant.BOTTOM_LEFT: // It will expand to the top return Position( bottom: parentSize.height - highlightPosition.bottom, right: parentSize.width - highlightPosition.left, ); - case 5: + case Quadrant.CENTER: // It will be centered final topHeightFromEdge = highlightPosition.top; final bottomHeightFromEdge = @@ -617,26 +644,26 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { } Position _getRightPositionFromQuadrant( - int quadrant, + Quadrant quadrant, Size parentSize, Position highlightPosition, ) { switch (quadrant) { - case 1: - case 2: + case Quadrant.TOP_RIGHT: + case Quadrant.TOP_LEFT: // It will expand to the bottom return Position( top: highlightPosition.top, left: highlightPosition.right, ); - case 3: - case 4: + case Quadrant.BOTTOM_RIGHT: + case Quadrant.BOTTOM_LEFT: // It will expand to the top return Position( bottom: parentSize.height - highlightPosition.bottom, left: highlightPosition.right, ); - case 5: + case Quadrant.CENTER: // It will be centered final topHeightFromEdge = highlightPosition.top; final bottomHeightFromEdge = @@ -678,13 +705,13 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { } Position _getUpPositionFromQuadrant( - int quadrant, + Quadrant quadrant, Size parentSize, Position highlightPosition, ) { switch (quadrant) { - case 1: - case 4: + case Quadrant.TOP_RIGHT: + case Quadrant.BOTTOM_RIGHT: // It will expand to the left final spacingFromTheRightEdge = parentSize.width - highlightPosition.right; @@ -692,14 +719,14 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { right: spacingFromTheRightEdge, bottom: parentSize.height - highlightPosition.top, ); - case 2: - case 3: + case Quadrant.TOP_LEFT: + case Quadrant.BOTTOM_LEFT: // It will expand to the right return Position( left: highlightPosition.left, bottom: parentSize.height - highlightPosition.top, ); - case 5: + case Quadrant.CENTER: final widthFromRightEdge = parentSize.width - highlightPosition.right; final widthFromLeftEdge = highlightPosition.left; final availableWidth = widthFromRightEdge > widthFromLeftEdge @@ -719,6 +746,29 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { throw ('Slide is outside the view area'); } } + + @override + Alignment getAlignment( + BuildContext context, + Position highlightPosition, + Size parentSize, + ) { + Quadrant quadrant = _getQuadrantFromRelativePosition( + highlightPosition, parentSize, direction); + + switch (quadrant) { + case Quadrant.TOP_RIGHT: + return Alignment.topRight; + case Quadrant.TOP_LEFT: + return Alignment.topLeft; + case Quadrant.BOTTOM_LEFT: + return Alignment.bottomLeft; + case Quadrant.BOTTOM_RIGHT: + return Alignment.bottomRight; + case Quadrant.CENTER: + return Alignment.center; + } + } } /// A bubble slide child with an absolute position on the screen. @@ -739,4 +789,13 @@ class AbsoluteBubbleSlideChild extends BubbleSlideChild { Size parentSize, ) => positionCalculator(parentSize); + + @override + Alignment getAlignment( + BuildContext context, + Position highlightPosition, + Size parentSize, + ) { + return Alignment.center; + } } From 18e3227ddb7de07e8af7d787597465f7f2fbd5ba Mon Sep 17 00:00:00 2001 From: Mario Date: Sat, 4 Dec 2021 23:22:49 -0600 Subject: [PATCH 15/16] wip: move logic to RelativeBubbleSlideChildBuilder --- example/lib/draggableShowcase.dart | 113 +++--- example/lib/main.dart | 5 - lib/src/slide.dart | 555 ++++++++--------------------- lib/src/utils.dart | 333 +++++++++++++++++ 4 files changed, 558 insertions(+), 448 deletions(-) diff --git a/example/lib/draggableShowcase.dart b/example/lib/draggableShowcase.dart index 259a9ab..74c7ad7 100644 --- a/example/lib/draggableShowcase.dart +++ b/example/lib/draggableShowcase.dart @@ -57,51 +57,85 @@ class BubbleShowcaseDraggableWidgetState BubbleSlide _draggableSlide(TextStyle textStyle) => RelativeBubbleSlide( widgetKey: _draggableSlideKey, - child: RelativeBubbleSlideChild( + child: RelativeBubbleSlideChildBuilder( middlePointHeight: middlePointHeight, middlePointWidth: middlePointWidth, - enableExtraSpace: true, direction: AxisDirection.down, - widget: Padding( - padding: const EdgeInsets.all(8.0), - child: SpeechBubble( - color: Colors.blue, - nipLocation: NipLocation.TOP_RIGHT, - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Example Slide', - style: textStyle.copyWith( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.only(right: 5), - child: Icon( - Icons.info_outline, - color: Colors.white, - ), + builder: ( + ctx, + highlightPosition, + slidePosition, + parentSize, + slideAlignment, + slideDirection, + ) { + NipLocation getNipLocation( + Alignment alignment, + AxisDirection direction, + ) { + if (alignment == Alignment.topLeft) { + return NipLocation.TOP_LEFT; + } else if (alignment == Alignment.topRight) { + return NipLocation.TOP_RIGHT; + } else if (alignment == Alignment.bottomLeft) { + return NipLocation.BOTTOM_LEFT; + } else if (alignment == Alignment.bottomRight) { + return NipLocation.BOTTOM_RIGHT; + } else { + switch (direction) { + case AxisDirection.up: + return NipLocation.TOP; + case AxisDirection.right: + return NipLocation.RIGHT; + case AxisDirection.down: + return NipLocation.BOTTOM; + case AxisDirection.left: + return NipLocation.LEFT; + } + } + } + + return Padding( + padding: const EdgeInsets.all(8.0), + child: SpeechBubble( + color: Colors.blue, + nipLocation: getNipLocation(slideAlignment, slideDirection), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Example Slide', + style: textStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, ), - Flexible( - child: Text( - 'Example of advanced positioning system.', - style: textStyle, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(right: 5), + child: Icon( + Icons.info_outline, + color: Colors.white, + ), ), - ) - ], - ), - ], + Flexible( + child: Text( + 'Example of advanced positioning system.', + style: textStyle, + ), + ) + ], + ), + ], + ), ), ), - ), - ), + ); + }, ), ); } @@ -184,8 +218,7 @@ class _BubbleShowcaseDraggableChildState ), // Center zone bottom (Horizontal) Positioned( - bottom: MediaQuery.of(context).size.height * - (middlePointHeight + 0.5 - 1), + bottom: MediaQuery.of(context).size.height * (middlePointHeight), left: MediaQuery.of(context).size.width * 0.055, right: MediaQuery.of(context).size.width * 0.055, child: const Divider( diff --git a/example/lib/main.dart b/example/lib/main.dart index 868d98d..fb6b50f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -201,7 +201,6 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { passThroughMode: PassthroughMode.INSIDE_WITH_NOTIFICATION, widgetKey: _thirdSlideKey, child: RelativeBubbleSlideChild( - enableExtraSpace: true, direction: AxisDirection.down, widget: Padding( padding: const EdgeInsets.all(8.0), @@ -237,7 +236,6 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { BubbleSlide _fourthSlide(TextStyle textStyle) => RelativeBubbleSlide( widgetKey: _fourthSlideKey, child: RelativeBubbleSlideChild( - enableExtraSpace: true, direction: AxisDirection.down, widget: Padding( padding: const EdgeInsets.all(8.0), @@ -283,7 +281,6 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { BubbleSlide _fifthSlide(TextStyle textStyle) => RelativeBubbleSlide( widgetKey: _fifthSlideKey, child: RelativeBubbleSlideChild( - enableExtraSpace: true, direction: AxisDirection.up, widget: Padding( padding: const EdgeInsets.all(8.0), @@ -332,7 +329,6 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { spreadRadius: 15, ), child: RelativeBubbleSlideChild( - enableExtraSpace: true, direction: AxisDirection.up, widget: Padding( padding: const EdgeInsets.all(16.0), @@ -378,7 +374,6 @@ class _BubbleShowcaseDemoWidget extends StatelessWidget { BubbleSlide _seventhSlide(TextStyle textStyle) => RelativeBubbleSlide( widgetKey: _seventhSlideKey, child: RelativeBubbleSlideChild( - enableExtraSpace: true, direction: AxisDirection.left, widget: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 8a86bc0..7e47ffa 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -274,29 +274,59 @@ class AbsoluteBubbleSlide extends BubbleSlide { /// A bubble slide child, holding a widget. abstract class BubbleSlideChild { + /// The direction of the slide + final AxisDirection direction; + /// The held widget. - final Widget widget; + final Widget? widget; + + /// Builder function + final Widget Function( + BuildContext ctx, + Position highlightPosition, + Position slidePosition, + Size parentSize, + Alignment slideAlignment, + AxisDirection slideDirection, + )? builder; /// Creates a new bubble slide child instance. const BubbleSlideChild({ required this.widget, + required this.builder, + required this.direction, }); /// Builds the bubble slide child widget. Widget build(BuildContext context, Position targetPosition, Size parentSize) { - Position position = getPosition(context, targetPosition, parentSize); + print("DEBUG => Hello world"); + Widget childWidget; + Position slidePosition = getPosition(context, targetPosition, parentSize); Alignment alignment = getAlignment(context, targetPosition, parentSize); - print("DEBUG => alignment: $alignment, position: $position"); + print("DEBUG => alignment: $alignment, slidePosition: $slidePosition"); + + if (builder != null) { + childWidget = builder!( + context, + targetPosition, + slidePosition, + parentSize, + alignment, + direction, + ); + } else { + childWidget = widget!; + } return Positioned( - top: position.top, - right: position.right, - bottom: position.bottom, - left: position.left, + top: slidePosition.top, + right: slidePosition.right, + bottom: slidePosition.bottom, + left: slidePosition.left, child: Align( alignment: alignment, - child: widget, + child: childWidget, ), ); } @@ -320,37 +350,14 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { /// The child direction. final AxisDirection direction; - /// Enables a new positioning system that will allow the child of the slide to - /// expand beyond the highlighted area's dimensions. - /// - /// Heavily assisted by using an `Align` widget to align it within the expanded space - final bool enableExtraSpace; - - /// Determines, in size a percentage from 0.15 to 0.45, the height of the parent container that will be - /// recognized as "Middle" space, starting from the center. - /// - /// Used by the automatic positioning system to determine - /// which positioning strategy to use. Defaults to 15% of the area from the middle to be counted as "center space". - final double middlePointHeight; - - /// Determines, in size a percentage from 0.15 to 0.45, the width of the parent container that will be - /// recognized as "Middle" space, starting from the center. - /// - /// Used by the automatic positioning system to determine - /// which positioning strategy to use. Defaults to 15% of the area from the middle to be counted as "center space". - final double middlePointWidth; - /// Creates a new relative bubble slide child instance. const RelativeBubbleSlideChild({ - required Widget widget, + required Widget? widget, this.direction = AxisDirection.down, - this.enableExtraSpace = false, - this.middlePointWidth = 0.15, - this.middlePointHeight = 0.15, - }) : assert(middlePointHeight >= 0 && middlePointHeight < 0.45), - assert(middlePointWidth >= 0 && middlePointWidth < 0.45), - super( + }) : super( + direction: direction, widget: widget, + builder: null, ); @override @@ -359,393 +366,83 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { Position highlightPosition, Size parentSize, ) { - if (enableExtraSpace) { - Quadrant quadrant = _getQuadrantFromRelativePosition( - highlightPosition, parentSize, direction); - - print('DEBUG => quadrant $quadrant'); - - switch (direction) { - case AxisDirection.up: - return _getUpPositionFromQuadrant( - quadrant, - parentSize, - highlightPosition, - ); - case AxisDirection.right: - return _getRightPositionFromQuadrant( - quadrant, - parentSize, - highlightPosition, - ); - case AxisDirection.left: - return _getLeftPositionFromQuadrant( - quadrant, - parentSize, - highlightPosition, - ); - default: - return _getDownPositionFromQuadrant( - quadrant, parentSize, highlightPosition); - } - } else { - switch (direction) { - case AxisDirection.up: - return Position( - right: parentSize.width - highlightPosition.right, - bottom: parentSize.height - highlightPosition.top, - left: highlightPosition.left, - ); - case AxisDirection.right: - return Position( - top: highlightPosition.top, - bottom: parentSize.height - highlightPosition.bottom, - right: parentSize.width - highlightPosition.left, - ); - case AxisDirection.left: - return Position( - top: highlightPosition.top, - bottom: parentSize.height - highlightPosition.bottom, - left: highlightPosition.right, - ); - default: - return Position( - top: highlightPosition.bottom, - right: parentSize.width - highlightPosition.right, - left: highlightPosition.left, - ); - } - } - } - - Quadrant _getQuadrantFromRelativePosition( - Position highlightPosition, - Size parentSize, - AxisDirection direction, - ) { - // Distance of the right point from the left edge - final r = highlightPosition.right; - // Distance of the left point from the left edge - final l = highlightPosition.left; - // Ditsance from the top point from the top - final t = highlightPosition.top; - // Distance from the bottom point from the top - final b = highlightPosition.bottom; - - // Distance of the right point from the right edge - final dr = parentSize.width - r; - // Distance of the left point from the left edge - final dl = l; - // Distance of the top point from the top - final dt = t; - // Distance of the bottom point from the bottom; - final db = parentSize.height - b; - - final w = parentSize.width; - final h = parentSize.height; - // Represents the boundaries x1 & y1 represent the positive axes from the middle of the parent - // While x2 & y2 represent the negative axes from the middle of the parent - - final middlePointWidthConverted = 0.5 + middlePointWidth; - final middlePointHeightConverted = 0.5 + middlePointHeight; - - final x1 = w * middlePointWidthConverted; - final x2 = w - (w * middlePointWidthConverted); - - final y1 = h - (h * middlePointHeightConverted); - final y2 = h * middlePointHeightConverted; - - // Mx & My represent the middle points of the axes of the highlighted item - final highlightAreaSize = Size(r - l, b - t); - final mx = l + highlightAreaSize.width / 2; - final my = t + highlightAreaSize.height / 2; - - // It leans to the right side if the distance from the right edge is less than from the right edge - final leansToRightSide = dr < dl; - - // It leans to the bottom side if the distance from the bottom edge is less than from the top edge - final leansToBottomSide = db < dt; - print('==================================='); - print('leansToBottomSide: $leansToBottomSide'); - print( - 'highlightPosition.bottom: ${highlightPosition.bottom}, highlightPosition.top: ${highlightPosition.top}', - ); - print('==================================='); - print('leansToRightSide: $leansToRightSide'); - print( - 'highlightPosition.right: ${highlightPosition.right}, highlightPosition.left: ${highlightPosition.left}', - ); - - final isHorizontal = - direction == AxisDirection.left || direction == AxisDirection.right; - - final isVertical = - direction == AxisDirection.up || direction == AxisDirection.down; - - // Calculate extremes first, cases where - // it might be in quadrant 5, but we cannot center due to possible - // collitions with the screen edges. - if (dr <= w * 0.05 && isVertical) { - // Extreme right (slide on top or bottom of highlighted area) - print('1st'); - if (leansToBottomSide) { - return Quadrant.BOTTOM_RIGHT; - } else { - return Quadrant.TOP_RIGHT; - } - } else if (dl <= w * 0.05 && isVertical) { - // Extreme left (slide on top or bottom of highlighted area) - print('2nd'); - if (leansToBottomSide) { - return Quadrant.BOTTOM_LEFT; - } else { - return Quadrant.TOP_LEFT; - } - } else if (db <= h * 0.05 && isHorizontal) { - // Extreme bottom (slide on left or right of highlighted area) - print('3rd'); - if (leansToRightSide) { - return Quadrant.BOTTOM_RIGHT; - } else { - return Quadrant.BOTTOM_LEFT; - } - } else if (dt <= h * 0.05 && isHorizontal) { - // Extreme top (slide on left or right of highlighted area) - print('4th'); - if (leansToRightSide) { - return Quadrant.TOP_RIGHT; - } else { - return Quadrant.TOP_LEFT; - } - } - - print("Quadrant calculated normally"); - - // Calculate quadrants normally - if (mx >= x1 && my <= y1) { - print("DEBUG => Top_right"); - return Quadrant.TOP_RIGHT; - } else if (mx <= x2 && my <= y1) { - print("DEBUG => Top_left"); - return Quadrant.TOP_LEFT; - } else if (mx <= x2 && my >= y2) { - print("DEBUG => Bottom_left"); - return Quadrant.BOTTOM_LEFT; - } else if (mx >= x1 && my >= y2) { - print("DEBUG => Bottom_right"); - return Quadrant.BOTTOM_RIGHT; - } else { - return Quadrant.CENTER; // center (Not totally within any other quadrant) - } - } - - Position _getDownPositionFromQuadrant( - Quadrant quadrant, - Size parentSize, - Position highlightPosition, - ) { - switch (quadrant) { - case Quadrant.TOP_RIGHT: - case Quadrant.BOTTOM_RIGHT: - // It will expand to the left - final spacingFromTheRightEdge = - parentSize.width - highlightPosition.right; - return Position( - right: spacingFromTheRightEdge, - top: highlightPosition.bottom, - ); - case Quadrant.TOP_LEFT: - case Quadrant.BOTTOM_LEFT: - // It will expand to the right + switch (direction) { + case AxisDirection.up: return Position( + right: parentSize.width - highlightPosition.right, + bottom: parentSize.height - highlightPosition.top, left: highlightPosition.left, - top: highlightPosition.bottom, ); - case Quadrant.CENTER: - final widthFromRightEdge = parentSize.width - highlightPosition.right; - final widthFromLeftEdge = highlightPosition.left; - final availableWidth = widthFromRightEdge > widthFromLeftEdge - ? highlightPosition.right - : highlightPosition.left; - - return Position( - top: highlightPosition.bottom, - right: widthFromRightEdge > widthFromLeftEdge - ? (widthFromRightEdge) - (availableWidth / 2) - : availableWidth / 2, - left: widthFromLeftEdge > widthFromRightEdge - ? (widthFromLeftEdge) + (availableWidth / 2) - : availableWidth / 2, - ); - default: - throw ('Slide is outside the view area'); - } - } - - Position _getLeftPositionFromQuadrant( - Quadrant quadrant, - Size parentSize, - Position highlightPosition, - ) { - switch (quadrant) { - case Quadrant.TOP_RIGHT: - case Quadrant.TOP_LEFT: - // It will expand to the bottom + case AxisDirection.right: return Position( top: highlightPosition.top, - right: parentSize.width - highlightPosition.left, - ); - case Quadrant.BOTTOM_RIGHT: - case Quadrant.BOTTOM_LEFT: - // It will expand to the top - return Position( bottom: parentSize.height - highlightPosition.bottom, right: parentSize.width - highlightPosition.left, ); - case Quadrant.CENTER: - // It will be centered - final topHeightFromEdge = highlightPosition.top; - final bottomHeightFromEdge = - parentSize.height - highlightPosition.bottom; - final availableHeight = topHeightFromEdge > bottomHeightFromEdge - ? (parentSize.height - highlightPosition.bottom) - - (parentSize.height - highlightPosition.top) - : (parentSize.height - highlightPosition.top) - - (parentSize.height - highlightPosition.bottom); - final highlightedItemSize = Size( - highlightPosition.right - highlightPosition.left, - highlightPosition.bottom - highlightPosition.top, - ); - double top; - double bottom; - if (topHeightFromEdge > bottomHeightFromEdge) { - top = (topHeightFromEdge) - - ((availableHeight / 2) + - (bottomHeightFromEdge / 2) + - highlightedItemSize.height); - bottom = (bottomHeightFromEdge / 2) - highlightedItemSize.height / 2; - } else { - top = (topHeightFromEdge / 2) - highlightedItemSize.height; - bottom = (bottomHeightFromEdge) - - ((availableHeight / 2) + - (topHeightFromEdge / 2) + - highlightedItemSize.height / 2); - } - - return Position( - top: top, - bottom: bottom, - right: parentSize.width - highlightPosition.left, - ); - - default: - throw ('Slide is outside the view area'); - } - } - - Position _getRightPositionFromQuadrant( - Quadrant quadrant, - Size parentSize, - Position highlightPosition, - ) { - switch (quadrant) { - case Quadrant.TOP_RIGHT: - case Quadrant.TOP_LEFT: - // It will expand to the bottom + case AxisDirection.left: return Position( top: highlightPosition.top, - left: highlightPosition.right, - ); - case Quadrant.BOTTOM_RIGHT: - case Quadrant.BOTTOM_LEFT: - // It will expand to the top - return Position( bottom: parentSize.height - highlightPosition.bottom, left: highlightPosition.right, ); - case Quadrant.CENTER: - // It will be centered - final topHeightFromEdge = highlightPosition.top; - final bottomHeightFromEdge = - parentSize.height - highlightPosition.bottom; - - final availableHeight = topHeightFromEdge > bottomHeightFromEdge - ? (parentSize.height - highlightPosition.bottom) - - (parentSize.height - highlightPosition.top) - : (parentSize.height - highlightPosition.top) - - (parentSize.height - highlightPosition.bottom); - final highlightedItemSize = Size( - highlightPosition.right - highlightPosition.left, - highlightPosition.bottom - highlightPosition.top, - ); - double top; - double bottom; - if (topHeightFromEdge > bottomHeightFromEdge) { - top = (topHeightFromEdge) - - ((availableHeight / 2) + - (bottomHeightFromEdge / 2) + - highlightedItemSize.height); - bottom = (bottomHeightFromEdge / 2) - highlightedItemSize.height / 2; - } else { - top = (topHeightFromEdge / 2) - highlightedItemSize.height; - bottom = (bottomHeightFromEdge) - - ((availableHeight / 2) + - (topHeightFromEdge / 2) + - highlightedItemSize.height / 2); - } - + default: return Position( - top: top, - bottom: bottom, - left: highlightPosition.right, + top: highlightPosition.bottom, + right: parentSize.width - highlightPosition.right, + left: highlightPosition.left, ); - default: - throw ('Slide is outside the view area'); } } - Position _getUpPositionFromQuadrant( - Quadrant quadrant, - Size parentSize, + @override + Alignment getAlignment( + BuildContext context, Position highlightPosition, + Size parentSize, ) { - switch (quadrant) { - case Quadrant.TOP_RIGHT: - case Quadrant.BOTTOM_RIGHT: - // It will expand to the left - final spacingFromTheRightEdge = - parentSize.width - highlightPosition.right; - return Position( - right: spacingFromTheRightEdge, - bottom: parentSize.height - highlightPosition.top, - ); - case Quadrant.TOP_LEFT: - case Quadrant.BOTTOM_LEFT: - // It will expand to the right - return Position( - left: highlightPosition.left, - bottom: parentSize.height - highlightPosition.top, - ); - case Quadrant.CENTER: - final widthFromRightEdge = parentSize.width - highlightPosition.right; - final widthFromLeftEdge = highlightPosition.left; - final availableWidth = widthFromRightEdge > widthFromLeftEdge - ? highlightPosition.right - : highlightPosition.left; + return Alignment.center; + } +} - return Position( - bottom: parentSize.height - highlightPosition.top, - right: widthFromRightEdge > widthFromLeftEdge - ? (widthFromRightEdge) - (availableWidth / 2) - : availableWidth / 2, - left: widthFromLeftEdge > widthFromRightEdge - ? (widthFromLeftEdge) + (availableWidth / 2) - : availableWidth / 2, +class RelativeBubbleSlideChildBuilder extends BubbleSlideChild { + /// The child direction. + final AxisDirection direction; + + /// Determines, in size a percentage from 0.15 to 0.45, the height of the parent container that will be + /// recognized as "Middle" space, starting from the center. + /// + /// Used by the automatic positioning system to determine + /// which positioning strategy to use. Defaults to 15% of the area from the middle to be counted as "center space". + final double middlePointHeight; + + /// Determines, in size a percentage from 0.15 to 0.45, the width of the parent container that will be + /// recognized as "Middle" space, starting from the center. + /// + /// Used by the automatic positioning system to determine + /// which positioning strategy to use. Defaults to 15% of the area from the middle to be counted as "center space". + final double middlePointWidth; + + final Widget Function( + BuildContext ctx, + Position highlightPosition, + Position slidePosition, + Size parentSize, + Alignment slideAlignment, + AxisDirection slideDirection, + ) builder; + + RelativeBubbleSlideChildBuilder({ + required this.builder, + this.direction = AxisDirection.down, + this.middlePointWidth = 0.15, + this.middlePointHeight = 0.15, + }) : assert(middlePointHeight >= 0 && middlePointHeight < 0.45), + assert(middlePointWidth >= 0 && middlePointWidth < 0.45), + super( + direction: direction, + widget: null, + builder: builder, ); - default: - throw ('Slide is outside the view area'); - } - } @override Alignment getAlignment( @@ -753,8 +450,14 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { Position highlightPosition, Size parentSize, ) { - Quadrant quadrant = _getQuadrantFromRelativePosition( - highlightPosition, parentSize, direction); + Quadrant quadrant = + AdvancedPositioningUtils.getQuadrantFromRelativePosition( + highlightPosition: highlightPosition, + parentSize: parentSize, + direction: direction, + middlePointHeight: middlePointHeight, + middlePointWidth: middlePointWidth, + ); switch (quadrant) { case Quadrant.TOP_RIGHT: @@ -769,6 +472,48 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { return Alignment.center; } } + + @override + Position getPosition( + BuildContext context, + Position highlightPosition, + Size parentSize, + ) { + Quadrant quadrant = + AdvancedPositioningUtils.getQuadrantFromRelativePosition( + highlightPosition: highlightPosition, + parentSize: parentSize, + direction: direction, + middlePointHeight: middlePointHeight, + middlePointWidth: middlePointWidth, + ); + + print('DEBUG => quadrant $quadrant'); + + switch (direction) { + case AxisDirection.up: + return AdvancedPositioningUtils.getUpPositionFromQuadrant( + quadrant, + parentSize, + highlightPosition, + ); + case AxisDirection.right: + return AdvancedPositioningUtils.getRightPositionFromQuadrant( + quadrant, + parentSize, + highlightPosition, + ); + case AxisDirection.left: + return AdvancedPositioningUtils.getLeftPositionFromQuadrant( + quadrant, + parentSize, + highlightPosition, + ); + default: + return AdvancedPositioningUtils.getDownPositionFromQuadrant( + quadrant, parentSize, highlightPosition); + } + } } /// A bubble slide child with an absolute position on the screen. @@ -780,7 +525,11 @@ class AbsoluteBubbleSlideChild extends BubbleSlideChild { const AbsoluteBubbleSlideChild({ required Widget widget, required this.positionCalculator, - }) : super(widget: widget); + }) : super( + widget: widget, + builder: null, + direction: AxisDirection.down, + ); @override Position getPosition( diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 10ee608..a4e0875 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -115,3 +115,336 @@ class OverlayClipper extends CustomClipper { @override bool shouldReclip(covariant CustomClipper oldClipper) => false; } + +class AdvancedPositioningUtils { + static Quadrant getQuadrantFromRelativePosition({ + required Position highlightPosition, + required Size parentSize, + required AxisDirection direction, + required double middlePointWidth, + required double middlePointHeight, + }) { + // Distance of the right point from the left edge + final r = highlightPosition.right; + // Distance of the left point from the left edge + final l = highlightPosition.left; + // Ditsance from the top point from the top + final t = highlightPosition.top; + // Distance from the bottom point from the top + final b = highlightPosition.bottom; + + // Distance of the right point from the right edge + final dr = parentSize.width - r; + // Distance of the left point from the left edge + final dl = l; + // Distance of the top point from the top + final dt = t; + // Distance of the bottom point from the bottom; + final db = parentSize.height - b; + + final w = parentSize.width; + final h = parentSize.height; + // Represents the boundaries x1 & y1 represent the positive axes from the middle of the parent + // While x2 & y2 represent the negative axes from the middle of the parent + + final middlePointWidthConverted = 0.5 + middlePointWidth; + final middlePointHeightConverted = 0.5 + middlePointHeight; + + final x1 = w * middlePointWidthConverted; + final x2 = w - (w * middlePointWidthConverted); + + final y1 = h - (h * middlePointHeightConverted); + final y2 = h * middlePointHeightConverted; + + // Mx & My represent the middle points of the axes of the highlighted item + final highlightAreaSize = Size(r - l, b - t); + final mx = l + highlightAreaSize.width / 2; + final my = t + highlightAreaSize.height / 2; + + // It leans to the right side if the distance from the right edge is less than from the right edge + final leansToRightSide = dr < dl; + + // It leans to the bottom side if the distance from the bottom edge is less than from the top edge + final leansToBottomSide = db < dt; + print('==================================='); + print('leansToBottomSide: $leansToBottomSide'); + print( + 'highlightPosition.bottom: ${highlightPosition.bottom}, highlightPosition.top: ${highlightPosition.top}', + ); + print('==================================='); + print('leansToRightSide: $leansToRightSide'); + print( + 'highlightPosition.right: ${highlightPosition.right}, highlightPosition.left: ${highlightPosition.left}', + ); + + final isHorizontal = + direction == AxisDirection.left || direction == AxisDirection.right; + + final isVertical = + direction == AxisDirection.up || direction == AxisDirection.down; + + // Calculate extremes first, cases where + // it might be in quadrant 5, but we cannot center due to possible + // collitions with the screen edges. + if (dr <= w * 0.05 && isVertical) { + // Extreme right (slide on top or bottom of highlighted area) + print('1st'); + if (leansToBottomSide) { + return Quadrant.BOTTOM_RIGHT; + } else { + return Quadrant.TOP_RIGHT; + } + } else if (dl <= w * 0.05 && isVertical) { + // Extreme left (slide on top or bottom of highlighted area) + print('2nd'); + if (leansToBottomSide) { + return Quadrant.BOTTOM_LEFT; + } else { + return Quadrant.TOP_LEFT; + } + } else if (db <= h * 0.05 && isHorizontal) { + // Extreme bottom (slide on left or right of highlighted area) + print('3rd'); + if (leansToRightSide) { + return Quadrant.BOTTOM_RIGHT; + } else { + return Quadrant.BOTTOM_LEFT; + } + } else if (dt <= h * 0.05 && isHorizontal) { + // Extreme top (slide on left or right of highlighted area) + print('4th'); + if (leansToRightSide) { + return Quadrant.TOP_RIGHT; + } else { + return Quadrant.TOP_LEFT; + } + } + + print("Quadrant calculated normally"); + + // Calculate quadrants normally + if (mx >= x1 && my <= y1) { + print("DEBUG => Top_right"); + return Quadrant.TOP_RIGHT; + } else if (mx <= x2 && my <= y1) { + print("DEBUG => Top_left"); + return Quadrant.TOP_LEFT; + } else if (mx <= x2 && my >= y2) { + print("DEBUG => Bottom_left"); + return Quadrant.BOTTOM_LEFT; + } else if (mx >= x1 && my >= y2) { + print("DEBUG => Bottom_right"); + return Quadrant.BOTTOM_RIGHT; + } else { + return Quadrant.CENTER; // center (Not totally within any other quadrant) + } + } + + static Position getDownPositionFromQuadrant( + Quadrant quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case Quadrant.TOP_RIGHT: + case Quadrant.BOTTOM_RIGHT: + // It will expand to the left + final spacingFromTheRightEdge = + parentSize.width - highlightPosition.right; + return Position( + right: spacingFromTheRightEdge, + top: highlightPosition.bottom, + ); + case Quadrant.TOP_LEFT: + case Quadrant.BOTTOM_LEFT: + // It will expand to the right + return Position( + left: highlightPosition.left, + top: highlightPosition.bottom, + ); + case Quadrant.CENTER: + final widthFromRightEdge = parentSize.width - highlightPosition.right; + final widthFromLeftEdge = highlightPosition.left; + final availableWidth = widthFromRightEdge > widthFromLeftEdge + ? highlightPosition.right + : highlightPosition.left; + + return Position( + top: highlightPosition.bottom, + right: widthFromRightEdge > widthFromLeftEdge + ? (widthFromRightEdge) - (availableWidth / 2) + : availableWidth / 2, + left: widthFromLeftEdge > widthFromRightEdge + ? (widthFromLeftEdge) + (availableWidth / 2) + : availableWidth / 2, + ); + default: + throw ('Slide is outside the view area'); + } + } + + static Position getLeftPositionFromQuadrant( + Quadrant quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case Quadrant.TOP_RIGHT: + case Quadrant.TOP_LEFT: + // It will expand to the bottom + return Position( + top: highlightPosition.top, + right: parentSize.width - highlightPosition.left, + ); + case Quadrant.BOTTOM_RIGHT: + case Quadrant.BOTTOM_LEFT: + // It will expand to the top + return Position( + bottom: parentSize.height - highlightPosition.bottom, + right: parentSize.width - highlightPosition.left, + ); + case Quadrant.CENTER: + // It will be centered + final topHeightFromEdge = highlightPosition.top; + final bottomHeightFromEdge = + parentSize.height - highlightPosition.bottom; + final availableHeight = topHeightFromEdge > bottomHeightFromEdge + ? (parentSize.height - highlightPosition.bottom) - + (parentSize.height - highlightPosition.top) + : (parentSize.height - highlightPosition.top) - + (parentSize.height - highlightPosition.bottom); + final highlightedItemSize = Size( + highlightPosition.right - highlightPosition.left, + highlightPosition.bottom - highlightPosition.top, + ); + double top; + double bottom; + if (topHeightFromEdge > bottomHeightFromEdge) { + top = (topHeightFromEdge) - + ((availableHeight / 2) + + (bottomHeightFromEdge / 2) + + highlightedItemSize.height); + bottom = (bottomHeightFromEdge / 2) - highlightedItemSize.height / 2; + } else { + top = (topHeightFromEdge / 2) - highlightedItemSize.height; + bottom = (bottomHeightFromEdge) - + ((availableHeight / 2) + + (topHeightFromEdge / 2) + + highlightedItemSize.height / 2); + } + + return Position( + top: top, + bottom: bottom, + right: parentSize.width - highlightPosition.left, + ); + + default: + throw ('Slide is outside the view area'); + } + } + + static Position getRightPositionFromQuadrant( + Quadrant quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case Quadrant.TOP_RIGHT: + case Quadrant.TOP_LEFT: + // It will expand to the bottom + return Position( + top: highlightPosition.top, + left: highlightPosition.right, + ); + case Quadrant.BOTTOM_RIGHT: + case Quadrant.BOTTOM_LEFT: + // It will expand to the top + return Position( + bottom: parentSize.height - highlightPosition.bottom, + left: highlightPosition.right, + ); + case Quadrant.CENTER: + // It will be centered + final topHeightFromEdge = highlightPosition.top; + final bottomHeightFromEdge = + parentSize.height - highlightPosition.bottom; + + final availableHeight = topHeightFromEdge > bottomHeightFromEdge + ? (parentSize.height - highlightPosition.bottom) - + (parentSize.height - highlightPosition.top) + : (parentSize.height - highlightPosition.top) - + (parentSize.height - highlightPosition.bottom); + final highlightedItemSize = Size( + highlightPosition.right - highlightPosition.left, + highlightPosition.bottom - highlightPosition.top, + ); + double top; + double bottom; + if (topHeightFromEdge > bottomHeightFromEdge) { + top = (topHeightFromEdge) - + ((availableHeight / 2) + + (bottomHeightFromEdge / 2) + + highlightedItemSize.height); + bottom = (bottomHeightFromEdge / 2) - highlightedItemSize.height / 2; + } else { + top = (topHeightFromEdge / 2) - highlightedItemSize.height; + bottom = (bottomHeightFromEdge) - + ((availableHeight / 2) + + (topHeightFromEdge / 2) + + highlightedItemSize.height / 2); + } + + return Position( + top: top, + bottom: bottom, + left: highlightPosition.right, + ); + default: + throw ('Slide is outside the view area'); + } + } + + static Position getUpPositionFromQuadrant( + Quadrant quadrant, + Size parentSize, + Position highlightPosition, + ) { + switch (quadrant) { + case Quadrant.TOP_RIGHT: + case Quadrant.BOTTOM_RIGHT: + // It will expand to the left + final spacingFromTheRightEdge = + parentSize.width - highlightPosition.right; + return Position( + right: spacingFromTheRightEdge, + bottom: parentSize.height - highlightPosition.top, + ); + case Quadrant.TOP_LEFT: + case Quadrant.BOTTOM_LEFT: + // It will expand to the right + return Position( + left: highlightPosition.left, + bottom: parentSize.height - highlightPosition.top, + ); + case Quadrant.CENTER: + final widthFromRightEdge = parentSize.width - highlightPosition.right; + final widthFromLeftEdge = highlightPosition.left; + final availableWidth = widthFromRightEdge > widthFromLeftEdge + ? highlightPosition.right + : highlightPosition.left; + + return Position( + bottom: parentSize.height - highlightPosition.top, + right: widthFromRightEdge > widthFromLeftEdge + ? (widthFromRightEdge) - (availableWidth / 2) + : availableWidth / 2, + left: widthFromLeftEdge > widthFromRightEdge + ? (widthFromLeftEdge) + (availableWidth / 2) + : availableWidth / 2, + ); + default: + throw ('Slide is outside the view area'); + } + } +} From d1d2a9051c93ac958cdc3cb94112f4ced15afb37 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 7 Dec 2021 23:23:51 -0600 Subject: [PATCH 16/16] wip: improve alignment auto detect --- example/lib/draggableShowcase.dart | 8 ++- lib/src/slide.dart | 79 +++++++++++++++++++++++++----- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/example/lib/draggableShowcase.dart b/example/lib/draggableShowcase.dart index 74c7ad7..99b30df 100644 --- a/example/lib/draggableShowcase.dart +++ b/example/lib/draggableShowcase.dart @@ -81,14 +81,18 @@ class BubbleShowcaseDraggableWidgetState return NipLocation.BOTTOM_LEFT; } else if (alignment == Alignment.bottomRight) { return NipLocation.BOTTOM_RIGHT; + } else if (alignment == Alignment.centerLeft) { + return NipLocation.LEFT; + } else if (alignment == Alignment.centerRight) { + return NipLocation.RIGHT; } else { switch (direction) { case AxisDirection.up: - return NipLocation.TOP; + return NipLocation.BOTTOM; case AxisDirection.right: return NipLocation.RIGHT; case AxisDirection.down: - return NipLocation.BOTTOM; + return NipLocation.TOP; case AxisDirection.left: return NipLocation.LEFT; } diff --git a/lib/src/slide.dart b/lib/src/slide.dart index 7e47ffa..4a3306c 100644 --- a/lib/src/slide.dart +++ b/lib/src/slide.dart @@ -104,7 +104,7 @@ abstract class BubbleSlide { } // Add BubbleSlide - if (child?.widget != null) { + if (child?.widget != null || child?.builder != null) { children.add( child!.build( context, @@ -302,11 +302,15 @@ abstract class BubbleSlideChild { print("DEBUG => Hello world"); Widget childWidget; Position slidePosition = getPosition(context, targetPosition, parentSize); - Alignment alignment = getAlignment(context, targetPosition, parentSize); + Alignment alignment = + getAlignment(context, targetPosition, parentSize, direction); - print("DEBUG => alignment: $alignment, slidePosition: $slidePosition"); + print( + "DEBUG => alignment: $alignment, direction: $direction, slidePosition: $slidePosition", + ); if (builder != null) { + print("DEBUG => Building off the builder"); childWidget = builder!( context, targetPosition, @@ -316,6 +320,7 @@ abstract class BubbleSlideChild { direction, ); } else { + print("DEBUG => Using the widget passed in props"); childWidget = widget!; } @@ -324,9 +329,12 @@ abstract class BubbleSlideChild { right: slidePosition.right, bottom: slidePosition.bottom, left: slidePosition.left, - child: Align( - alignment: alignment, - child: childWidget, + child: Container( + color: Colors.black, + child: Align( + alignment: alignment, + child: childWidget, + ), ), ); } @@ -342,6 +350,7 @@ abstract class BubbleSlideChild { BuildContext context, Position highlightPosition, Size parentSize, + AxisDirection direction, ); } @@ -399,6 +408,7 @@ class RelativeBubbleSlideChild extends BubbleSlideChild { BuildContext context, Position highlightPosition, Size parentSize, + AxisDirection direction, ) { return Alignment.center; } @@ -449,6 +459,7 @@ class RelativeBubbleSlideChildBuilder extends BubbleSlideChild { BuildContext context, Position highlightPosition, Size parentSize, + AxisDirection direction, ) { Quadrant quadrant = AdvancedPositioningUtils.getQuadrantFromRelativePosition( @@ -461,15 +472,60 @@ class RelativeBubbleSlideChildBuilder extends BubbleSlideChild { switch (quadrant) { case Quadrant.TOP_RIGHT: - return Alignment.topRight; + switch (direction) { + case AxisDirection.down: + return Alignment.topRight; + case AxisDirection.up: + return Alignment.bottomRight; + case AxisDirection.right: + return Alignment.topLeft; + case AxisDirection.left: + return Alignment.topRight; + } case Quadrant.TOP_LEFT: - return Alignment.topLeft; + switch (direction) { + case AxisDirection.down: + return Alignment.topLeft; + case AxisDirection.up: + return Alignment.bottomLeft; + case AxisDirection.right: + return Alignment.topLeft; + case AxisDirection.left: + return Alignment.topRight; + } case Quadrant.BOTTOM_LEFT: - return Alignment.bottomLeft; + switch (direction) { + case AxisDirection.down: + return Alignment.topLeft; + case AxisDirection.up: + return Alignment.bottomLeft; + case AxisDirection.right: + return Alignment.bottomLeft; + case AxisDirection.left: + return Alignment.bottomRight; + } case Quadrant.BOTTOM_RIGHT: - return Alignment.bottomRight; + switch (direction) { + case AxisDirection.down: + return Alignment.topRight; + case AxisDirection.up: + return Alignment.bottomRight; + case AxisDirection.right: + return Alignment.bottomLeft; + case AxisDirection.left: + return Alignment.bottomRight; + } case Quadrant.CENTER: - return Alignment.center; + switch (direction) { + case AxisDirection.down: + return Alignment.topCenter; + case AxisDirection.up: + return Alignment.bottomCenter; + case AxisDirection.left: + return Alignment.centerRight; + case AxisDirection.right: + return Alignment.centerLeft; + } } } @@ -544,6 +600,7 @@ class AbsoluteBubbleSlideChild extends BubbleSlideChild { BuildContext context, Position highlightPosition, Size parentSize, + AxisDirection direction, ) { return Alignment.center; }