diff --git a/lib/landing_page.dart b/lib/landing_page.dart index 90cce786..5febcf56 100644 --- a/lib/landing_page.dart +++ b/lib/landing_page.dart @@ -5,6 +5,7 @@ import 'package:doctor_app_flutter/screens/doctor/services_screen.dart'; import 'package:doctor_app_flutter/widgets/shared/app_drawer_widget.dart'; import 'package:doctor_app_flutter/widgets/shared/bottom_nav_bar.dart'; import 'package:doctor_app_flutter/widgets/shared/profile_image_widget.dart'; +import 'package:doctor_app_flutter/widgets/shared/user-guid/app_showcase_widget.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hexcolor/hexcolor.dart'; @@ -59,7 +60,9 @@ class _LandingPageState extends State { physics: NeverScrollableScrollPhysics(), controller: pageController, children: [ - DashboardScreen(), + ShowCaseWidget( + builder: Builder(builder: (context) => DashboardScreen()), + ), MessagesScreen(), MyScheduleScreen(), ServicesScreen() diff --git a/lib/screens/dashboard_screen.dart b/lib/screens/dashboard_screen.dart index 037b344d..ad64d89a 100644 --- a/lib/screens/dashboard_screen.dart +++ b/lib/screens/dashboard_screen.dart @@ -17,6 +17,8 @@ import 'package:doctor_app_flutter/util/dr_app_shared_pref.dart'; import 'package:doctor_app_flutter/util/helpers.dart'; import 'package:doctor_app_flutter/util/translations_delegate_base.dart'; import 'package:doctor_app_flutter/widgets/shared/app_scaffold_widget.dart'; +import 'package:doctor_app_flutter/widgets/shared/user-guid/app_showcase.dart'; +import 'package:doctor_app_flutter/widgets/shared/user-guid/app_showcase_widget.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; @@ -49,9 +51,13 @@ class _DashboardScreenState extends State { AuthProvider authProvider; bool isLoading = false; ProjectProvider projectsProvider; + BuildContext myContext; + + GlobalKey _one = GlobalKey(); @override Widget build(BuildContext context) { + myContext = context; hospitalProvider = Provider.of(context); authProvider = Provider.of(context); projectsProvider = Provider.of(context); @@ -75,28 +81,49 @@ class _DashboardScreenState extends State { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - margin: EdgeInsets.only(left: 10, top: 10), - child: AppText( - authProvider.selectedClinicName != null - ? authProvider.selectedClinicName - : '', - fontWeight: FontWeight.bold, - fontSize: SizeConfig.textMultiplier * 2.5, + Expanded( + flex: 1, + child: Container( + margin: EdgeInsets.only(left: 10, top: 10), + child: AppText( + authProvider.selectedClinicName != null + ? authProvider.selectedClinicName + : '', + fontWeight: FontWeight.bold, + fontSize: SizeConfig.textMultiplier * 2.5, + ), + alignment: Alignment.centerLeft, ), - alignment: Alignment.centerLeft, + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + InkWell( + onTap: () { + showCupertinoPicker( + context: context, + actionList: authProvider.doctorsClinicList); + }, + child: Container( + margin: + EdgeInsets.only(left: 5, top: 10, right: 5), + child: Icon( + Icons.settings, + size: SizeConfig.textMultiplier * 2.5, + )), + ), + ], ), InkWell( onTap: () { - showCupertinoPicker( - context: context, - actionList: authProvider.doctorsClinicList); + ShowCaseWidget.of(context).startShowCase([_one]); }, child: Container( - margin: - EdgeInsets.only(left: 10, top: 10, right: 10), + margin: EdgeInsets.only(left: 5, top: 10, right: 5), child: Icon( - Icons.settings, + Icons.info, size: SizeConfig.textMultiplier * 2.5, )), ) @@ -118,28 +145,36 @@ class _DashboardScreenState extends State { children: [ Expanded( flex: 2, - child: RoundedContainer( - child: CircularPercentIndicator( - radius: 100, - animation: true, - animationDuration: 1200, - lineWidth: 7.0, - percent: .75, - center: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AppText("38", - fontWeight: FontWeight.bold, - fontSize: SizeConfig.textMultiplier * 4), - AppText(TranslationBase.of(context).outPatients, - fontWeight: FontWeight.normal, - fontSize: SizeConfig.textMultiplier * 1.1, - color: Colors.grey[800]), - ], + child: AppShowcase( + key: _one, + description: "Test User Guid", + onSkipClick: () { + ShowCaseWidget.of(context).dismiss(); + }, + child: RoundedContainer( + child: CircularPercentIndicator( + radius: 100, + animation: true, + animationDuration: 1200, + lineWidth: 7.0, + percent: .75, + center: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppText("38", + fontWeight: FontWeight.bold, + fontSize: SizeConfig.textMultiplier * 4), + AppText( + TranslationBase.of(context).outPatients, + fontWeight: FontWeight.normal, + fontSize: SizeConfig.textMultiplier * 1.1, + color: Colors.grey[800]), + ], + ), + circularStrokeCap: CircularStrokeCap.butt, + backgroundColor: Colors.blueGrey[100], + progressColor: Colors.red, ), - circularStrokeCap: CircularStrokeCap.butt, - backgroundColor: Colors.blueGrey[100], - progressColor: Colors.red, ), ), ), diff --git a/lib/widgets/shared/user-guid/app_anchored_overlay_widget.dart b/lib/widgets/shared/user-guid/app_anchored_overlay_widget.dart new file mode 100644 index 00000000..8a4891fd --- /dev/null +++ b/lib/widgets/shared/user-guid/app_anchored_overlay_widget.dart @@ -0,0 +1,183 @@ +/* + * Copyright © 2020, Simform Solutions + * All rights reserved. + * https://github.com/simformsolutions/flutter_showcaseview + */ + +/* +Customized By: Ibrahim Albitar + +*/ + +import 'package:flutter/material.dart'; + +/// Displays an overlay Widget anchored directly above the center of this +/// [AnchoredOverlay]. +/// +/// The overlay Widget is created by invoking the provided [overlayBuilder]. +/// +/// The [anchor] position is provided to the [overlayBuilder], but the builder +/// does not have to respect it. In other words, the [overlayBuilder] can +/// interpret the meaning of "anchor" however it wants - the overlay will not +/// be forced to be centered about the [anchor]. +/// +/// The overlay built by this [AnchoredOverlay] can be conditionally shown +/// and hidden by settings the [showOverlay] property to true or false. +/// +/// The [overlayBuilder] is invoked every time this Widget is rebuilt. +/// +class AnchoredOverlay extends StatelessWidget { + final bool showOverlay; + final Widget Function(BuildContext, Rect anchorBounds, Offset anchor) + overlayBuilder; + final Widget child; + + AnchoredOverlay({ + key, + this.showOverlay = false, + this.overlayBuilder, + this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return OverlayBuilder( + showOverlay: showOverlay, + overlayBuilder: (BuildContext overlayContext) { + // To calculate the "anchor" point we grab the render box of + // our parent Container and then we find the center of that box. + RenderBox box = context.findRenderObject() as RenderBox; + final topLeft = + box.size.topLeft(box.localToGlobal(const Offset(0.0, 0.0))); + final bottomRight = + box.size.bottomRight(box.localToGlobal(const Offset(0.0, 0.0))); + final Rect anchorBounds = Rect.fromLTRB( + topLeft.dx, + topLeft.dy, + bottomRight.dx, + bottomRight.dy, + ); + final anchorCenter = box.size.center(topLeft); + return overlayBuilder(overlayContext, anchorBounds, anchorCenter); + }, + child: child, + ); + }, + ); + } +} + +// +// Displays an overlay Widget as constructed by the given [overlayBuilder]. +// +// The overlay built by the [overlayBuilder] can be conditionally shown and hidden by settings the [showOverlay] +// property to true or false. +// +// The [overlayBuilder] is invoked every time this Widget is rebuilt. +// +// Implementation note: the reason we rebuild the overlay every time our state changes is because there doesn't seem +// to be any better way to invalidate the overlay itself than to invalidate this Widget. +// Remember, overlay Widgets exist in [OverlayEntry]s which are inaccessible to outside Widgets. +// But if a better approach is found then feel free to use it. +// +class OverlayBuilder extends StatefulWidget { + final bool showOverlay; + final Widget Function(BuildContext) overlayBuilder; + final Widget child; + + OverlayBuilder({ + key, + this.showOverlay = false, + this.overlayBuilder, + this.child, + }) : super(key: key); + + @override + _OverlayBuilderState createState() => _OverlayBuilderState(); +} + +class _OverlayBuilderState extends State { + OverlayEntry _overlayEntry; + + @override + void initState() { + super.initState(); + + if (widget.showOverlay) { + WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay()); + } + } + + @override + void didUpdateWidget(OverlayBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay()); + } + + @override + void reassemble() { + super.reassemble(); + WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay()); + } + + @override + void dispose() { + if (isShowingOverlay()) { + hideOverlay(); + } + + super.dispose(); + } + + bool isShowingOverlay() => _overlayEntry != null; + + void showOverlay() { + if (_overlayEntry == null) { + // Create the overlay. + _overlayEntry = OverlayEntry( + builder: widget.overlayBuilder, + ); + addToOverlay(_overlayEntry); + } else { + // Rebuild overlay. + buildOverlay(); + } + } + + void addToOverlay(OverlayEntry overlayEntry) async { + Overlay.of(context).insert(overlayEntry); + final overlay = Overlay.of(context); + if (overlayEntry == null) + WidgetsBinding.instance + .addPostFrameCallback((_) => overlay.insert(overlayEntry)); + } + + void hideOverlay() { + if (_overlayEntry != null) { + _overlayEntry.remove(); + _overlayEntry = null; + } + } + + void syncWidgetAndOverlay() { + if (isShowingOverlay() && !widget.showOverlay) { + hideOverlay(); + } else if (!isShowingOverlay() && widget.showOverlay) { + showOverlay(); + } + } + + void buildOverlay() async { + WidgetsBinding.instance + .addPostFrameCallback((_) => _overlayEntry?.markNeedsBuild()); + } + + @override + Widget build(BuildContext context) { + buildOverlay(); + + return widget.child; + } +} diff --git a/lib/widgets/shared/user-guid/app_get_position.dart b/lib/widgets/shared/user-guid/app_get_position.dart new file mode 100644 index 00000000..c0430994 --- /dev/null +++ b/lib/widgets/shared/user-guid/app_get_position.dart @@ -0,0 +1,75 @@ +/* + * Copyright © 2020, Simform Solutions + * All rights reserved. + * https://github.com/simformsolutions/flutter_showcaseview + */ + +/* +Customized By: Ibrahim Albitar + +*/ +import 'package:flutter/material.dart'; + +class GetPosition { + final GlobalKey key; + + GetPosition({this.key}); + + Rect getRect() { + RenderBox box = key.currentContext.findRenderObject(); + + final topLeft = box.size.topLeft(box.localToGlobal(const Offset(0.0, 0.0))); + final bottomRight = + box.size.bottomRight(box.localToGlobal(const Offset(0.0, 0.0))); + + Rect rect = Rect.fromLTRB( + topLeft.dx, + topLeft.dy, + bottomRight.dx, + bottomRight.dy, + ); + return rect; + } + + ///Get the bottom position of the widget + double getBottom() { + RenderBox box = key.currentContext.findRenderObject(); + final bottomRight = + box.size.bottomRight(box.localToGlobal(const Offset(0.0, 0.0))); + return bottomRight.dy; + } + + ///Get the top position of the widget + double getTop() { + RenderBox box = key.currentContext.findRenderObject(); + final topLeft = box.size.topLeft(box.localToGlobal(const Offset(0.0, 0.0))); + return topLeft.dy; + } + + ///Get the left position of the widget + double getLeft() { + RenderBox box = key.currentContext.findRenderObject(); + final topLeft = box.size.topLeft(box.localToGlobal(const Offset(0.0, 0.0))); + return topLeft.dx; + } + + ///Get the right position of the widget + double getRight() { + RenderBox box = key.currentContext.findRenderObject(); + final bottomRight = + box.size.bottomRight(box.localToGlobal(const Offset(0.0, 0.0))); + return bottomRight.dx; + } + + double getHeight() { + return getBottom() - getTop(); + } + + double getWidth() { + return getRight() - getLeft(); + } + + double getCenter() { + return (getLeft() + getRight()) / 2; + } +} diff --git a/lib/widgets/shared/user-guid/app_shape_painter.dart b/lib/widgets/shared/user-guid/app_shape_painter.dart new file mode 100644 index 00000000..925d18d0 --- /dev/null +++ b/lib/widgets/shared/user-guid/app_shape_painter.dart @@ -0,0 +1,42 @@ +/* + * Copyright © 2020, Simform Solutions + * All rights reserved. + * https://github.com/simformsolutions/flutter_showcaseview + */ + +/* +Customized By: Ibrahim Albitar + +*/ + +import 'package:flutter/material.dart'; + +class ShapePainter extends CustomPainter { + Rect rect; + final ShapeBorder shapeBorder; + final Color color; + final double opacity; + + ShapePainter({ + @required this.rect, + this.color, + this.shapeBorder, + this.opacity, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint(); + paint.color = color.withOpacity(opacity); + RRect outer = + RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(0)); + + double radius = shapeBorder == CircleBorder() ? 50 : 3; + + RRect inner = RRect.fromRectAndRadius(rect, Radius.circular(radius)); + canvas.drawDRRect(outer, inner, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/widgets/shared/user-guid/app_showcase.dart b/lib/widgets/shared/user-guid/app_showcase.dart new file mode 100644 index 00000000..38625279 --- /dev/null +++ b/lib/widgets/shared/user-guid/app_showcase.dart @@ -0,0 +1,349 @@ +/* + * Copyright © 2020, Simform Solutions + * All rights reserved. + * https://github.com/simformsolutions/flutter_showcaseview + */ + +/* +Customized By: Ibrahim Albitar + +*/ + +import 'package:doctor_app_flutter/widgets/shared/app_texts_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +import 'app_anchored_overlay_widget.dart'; +import 'app_get_position.dart'; +import 'app_shape_painter.dart'; +import 'app_showcase_widget.dart'; +import 'app_tool_tip_widget.dart'; + +class AppShowcase extends StatefulWidget { + final Widget child; + final String title; + final String description; + final ShapeBorder shapeBorder; + final TextStyle titleTextStyle; + final TextStyle descTextStyle; + final GlobalKey key; + final Color overlayColor; + final double overlayOpacity; + final Widget container; + final Color showcaseBackgroundColor; + final Color textColor; + final bool showArrow; + final double height; + final double width; + final Duration animationDuration; + final VoidCallback onToolTipClick; + final VoidCallback onTargetClick; + final VoidCallback onSkipClick; + final bool disposeOnTap; + final bool disableAnimation; + + const AppShowcase( + {@required this.key, + @required this.child, + this.title, + @required this.description, + this.shapeBorder, + this.overlayColor = Colors.black, + this.overlayOpacity = 0.75, + this.titleTextStyle, + this.descTextStyle, + this.showcaseBackgroundColor = Colors.white, + this.textColor = Colors.black, + this.showArrow = true, + this.onTargetClick, + this.onSkipClick, + this.disposeOnTap, + this.animationDuration = const Duration(milliseconds: 2000), + this.disableAnimation = false}) + : height = null, + width = null, + container = null, + this.onToolTipClick = null, + assert(overlayOpacity >= 0.0 && overlayOpacity <= 1.0, + "overlay opacity should be >= 0.0 and <= 1.0."), + assert( + onTargetClick == null + ? true + : (disposeOnTap == null ? false : true), + "disposeOnTap is required if you're using onTargetClick"), + assert( + disposeOnTap == null + ? true + : (onTargetClick == null ? false : true), + "onTargetClick is required if you're using disposeOnTap"), + assert(key != null || + child != null || + title != null || + showArrow != null || + description != null || + shapeBorder != null || + overlayColor != null || + titleTextStyle != null || + descTextStyle != null || + showcaseBackgroundColor != null || + textColor != null || + shapeBorder != null || + animationDuration != null); + + const AppShowcase.withWidget( + {this.key, + @required this.child, + @required this.container, + @required this.height, + @required this.width, + this.title, + this.description, + this.shapeBorder, + this.overlayColor = Colors.black, + this.overlayOpacity = 0.75, + this.titleTextStyle, + this.descTextStyle, + this.showcaseBackgroundColor = Colors.white, + this.textColor = Colors.black, + this.onTargetClick, + this.onSkipClick, + this.disposeOnTap, + this.animationDuration = const Duration(milliseconds: 2000), + this.disableAnimation = false}) + : this.showArrow = false, + this.onToolTipClick = null, + assert(overlayOpacity >= 0.0 && overlayOpacity <= 1.0, + "overlay opacity should be >= 0.0 and <= 1.0."), + assert(key != null || + child != null || + title != null || + description != null || + shapeBorder != null || + overlayColor != null || + titleTextStyle != null || + descTextStyle != null || + showcaseBackgroundColor != null || + textColor != null || + shapeBorder != null || + animationDuration != null); + + @override + _AppShowcaseState createState() => _AppShowcaseState(); +} + +class _AppShowcaseState extends State + with TickerProviderStateMixin { + bool _showShowCase = false; + Animation _slideAnimation; + AnimationController _slideAnimationController; + + GetPosition position; + + @override + void initState() { + super.initState(); + + _slideAnimationController = AnimationController( + duration: widget.animationDuration, + vsync: this, + )..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + _slideAnimationController.reverse(); + } + if (_slideAnimationController.isDismissed) { + if (!widget.disableAnimation) { + _slideAnimationController.forward(); + } + } + }); + + _slideAnimation = CurvedAnimation( + parent: _slideAnimationController, + curve: Curves.easeInOut, + ); + + position = GetPosition(key: widget.key); + } + + @override + void dispose() { + _slideAnimationController.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + showOverlay(); + } + + /// + /// show overlay if there is any target widget + /// + void showOverlay() { + GlobalKey activeStep = ShowCaseWidget.activeTargetWidget(context); + setState(() { + _showShowCase = activeStep == widget.key; + }); + + if (activeStep == widget.key) { + if (!widget.disableAnimation) { + _slideAnimationController.forward(); + } + } + } + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return AnchoredOverlay( + overlayBuilder: (BuildContext context, Rect rectBound, Offset offset) => + buildOverlayOnTarget(offset, rectBound.size, rectBound, size), + showOverlay: true, + child: widget.child, + ); + } + + _nextIfAny() { + ShowCaseWidget.of(context).completed(widget.key); + if (!widget.disableAnimation) { + _slideAnimationController.forward(); + } + } + + _getOnTargetTap() { + if (widget.disposeOnTap == true) { + return widget.onTargetClick == null + ? () { + ShowCaseWidget.of(context).dismiss(); + } + : () { + ShowCaseWidget.of(context).dismiss(); + widget.onTargetClick(); + }; + } else { + return widget.onTargetClick ?? _nextIfAny; + } + } + + _getOnTooltipTap() { + if (widget.disposeOnTap == true) { + return widget.onToolTipClick == null + ? () { + ShowCaseWidget.of(context).dismiss(); + } + : () { + ShowCaseWidget.of(context).dismiss(); + widget.onToolTipClick(); + }; + } else { + return widget.onToolTipClick ?? () {}; + } + } + + buildOverlayOnTarget( + Offset offset, + Size size, + Rect rectBound, + Size screenSize, + ) => + Visibility( + visible: _showShowCase, + maintainAnimation: true, + maintainState: true, + child: Stack( + children: [ + GestureDetector( + onTap: _nextIfAny, + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: CustomPaint( + painter: ShapePainter( + opacity: widget.overlayOpacity, + rect: position.getRect(), + shapeBorder: widget.shapeBorder, + color: widget.overlayColor), + ), + ), + ), + _TargetWidget( + offset: offset, + size: size, + onTap: _getOnTargetTap(), + shapeBorder: widget.shapeBorder, + ), + AppToolTipWidget( + position: position, + offset: offset, + screenSize: screenSize, + title: widget.title, + description: widget.description, + animationOffset: _slideAnimation, + titleTextStyle: widget.titleTextStyle, + descTextStyle: widget.descTextStyle, + container: widget.container, + tooltipColor: widget.showcaseBackgroundColor, + textColor: widget.textColor, + showArrow: widget.showArrow, + contentHeight: widget.height, + contentWidth: widget.width, + onTooltipTap: _getOnTooltipTap(), + ), + GestureDetector( + child: AppText( + "Skip", + color: Colors.white, + fontSize: 20, + marginRight: 15, + marginLeft: 15, + marginTop: 15, + ), + onTap: widget.onSkipClick) + ], + ), + ); +} + +class _TargetWidget extends StatelessWidget { + final Offset offset; + final Size size; + final Animation widthAnimation; + final VoidCallback onTap; + final ShapeBorder shapeBorder; + + _TargetWidget({ + Key key, + @required this.offset, + this.size, + this.widthAnimation, + this.onTap, + this.shapeBorder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + top: offset.dy, + left: offset.dx, + child: FractionalTranslation( + translation: const Offset(-0.5, -0.5), + child: GestureDetector( + onTap: onTap, + child: Container( + height: size.height + 16, + width: size.width + 16, + decoration: ShapeDecoration( + shape: shapeBorder ?? + RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/shared/user-guid/app_showcase_widget.dart b/lib/widgets/shared/user-guid/app_showcase_widget.dart new file mode 100644 index 00000000..07577b3b --- /dev/null +++ b/lib/widgets/shared/user-guid/app_showcase_widget.dart @@ -0,0 +1,97 @@ +/* + * Copyright © 2020, Simform Solutions + * All rights reserved. + * https://github.com/simformsolutions/flutter_showcaseview + */ + +/* +Customized By: Ibrahim Albitar + +*/ + +import 'package:flutter/material.dart'; + +class ShowCaseWidget extends StatefulWidget { + final Builder builder; + final VoidCallback onFinish; + + const ShowCaseWidget({@required this.builder, this.onFinish}); + + static activeTargetWidget(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType<_InheritedShowCaseView>() + .activeWidgetIds; + } + + static ShowCaseWidgetState of(BuildContext context) { + ShowCaseWidgetState state = + context.findAncestorStateOfType(); + if (state != null) { + return context.findAncestorStateOfType(); + } else { + throw Exception('Please provide ShowCaseView context'); + } + } + + @override + ShowCaseWidgetState createState() => ShowCaseWidgetState(); +} + +class ShowCaseWidgetState extends State { + List ids; + int activeWidgetId; + + void startShowCase(List widgetIds) { + setState(() { + this.ids = widgetIds; + activeWidgetId = 0; + }); + } + + void completed(GlobalKey id) { + if (ids != null && ids[activeWidgetId] == id) { + setState(() { + ++activeWidgetId; + + if (activeWidgetId >= ids.length) { + _cleanupAfterSteps(); + if (widget.onFinish != null) { + widget.onFinish(); + } + } + }); + } + } + + void dismiss() { + setState(() { + _cleanupAfterSteps(); + }); + } + + void _cleanupAfterSteps() { + ids = null; + activeWidgetId = null; + } + + @override + Widget build(BuildContext context) { + return _InheritedShowCaseView( + child: widget.builder, + activeWidgetIds: ids?.elementAt(activeWidgetId), + ); + } +} + +class _InheritedShowCaseView extends InheritedWidget { + final GlobalKey activeWidgetIds; + + _InheritedShowCaseView({ + @required this.activeWidgetIds, + @required child, + }) : super(child: child); + + @override + bool updateShouldNotify(_InheritedShowCaseView oldWidget) => + oldWidget.activeWidgetIds != activeWidgetIds; +} diff --git a/lib/widgets/shared/user-guid/app_tool_tip_widget.dart b/lib/widgets/shared/user-guid/app_tool_tip_widget.dart new file mode 100644 index 00000000..4d95324b --- /dev/null +++ b/lib/widgets/shared/user-guid/app_tool_tip_widget.dart @@ -0,0 +1,289 @@ +/* + * Copyright © 2020, Simform Solutions + * All rights reserved. + * https://github.com/simformsolutions/flutter_showcaseview + */ + +/* +Customized By: Ibrahim Albitar + +*/ + +import 'package:doctor_app_flutter/icons_app/doctor_app_icons.dart'; +import 'package:doctor_app_flutter/widgets/shared/app_texts_widget.dart'; +import 'package:flutter/material.dart'; +import 'app_get_position.dart'; + +class AppToolTipWidget extends StatelessWidget { + final GetPosition position; + final Offset offset; + final Size screenSize; + final String title; + final String description; + final Animation animationOffset; + final TextStyle titleTextStyle; + final TextStyle descTextStyle; + final Widget container; + final Color tooltipColor; + final Color textColor; + final bool showArrow; + final double contentHeight; + final double contentWidth; + static bool isArrowUp; + final VoidCallback onTooltipTap; + + AppToolTipWidget({ + this.position, + this.offset, + this.screenSize, + this.title, + this.description, + this.animationOffset, + this.titleTextStyle, + this.descTextStyle, + this.container, + this.tooltipColor, + this.textColor, + this.showArrow, + this.contentHeight, + this.contentWidth, + this.onTooltipTap, + }); + + bool isCloseToTopOrBottom(Offset position) { + double height = 120; + if (contentHeight != null) { + height = contentHeight; + } + return (screenSize.height - position.dy) <= height; + } + + String findPositionForContent(Offset position) { + if (isCloseToTopOrBottom(position)) { + return 'ABOVE'; + } else { + return 'BELOW'; + } + } + + double _getTooltipWidth() { + double titleLength = title == null ? 0 : (title.length * 10.0); + double descriptionLength = (description.length * 7.0); + if (titleLength > descriptionLength) { + return titleLength + 10; + } else { + return descriptionLength + 10; + } + } + + bool _isLeft() { + double screenWidth = screenSize.width / 3; + return !(screenWidth <= position.getCenter()); + } + + bool _isRight() { + double screenWidth = screenSize.width / 3; + return ((screenWidth * 2) <= position.getCenter()); + } + + double _getLeft() { + if (_isLeft()) { + double leftPadding = position.getCenter() - (_getTooltipWidth() * 0.1); + if (leftPadding + _getTooltipWidth() > screenSize.width) { + leftPadding = (screenSize.width - 20) - _getTooltipWidth(); + } + if (leftPadding < 20) { + leftPadding = 14; + } + return leftPadding; + } else if (!(_isRight())) { + return position.getCenter() - (_getTooltipWidth() * 0.5); + } else { + return null; + } + } + + double _getRight() { + if (_isRight()) { + double rightPadding = position.getCenter() + (_getTooltipWidth() / 2); + if (rightPadding + _getTooltipWidth() > screenSize.width) { + rightPadding = 14; + } + return rightPadding; + } else if (!(_isLeft())) { + return position.getCenter() - (_getTooltipWidth() * 0.5); + } else { + return null; + } + } + + double _getSpace() { + double space = position.getCenter() - (contentWidth / 2); + if (space + contentWidth > screenSize.width) { + space = screenSize.width - contentWidth - 8; + } else if (space < (contentWidth / 2)) { + space = 16; + } + return space; + } + + @override + Widget build(BuildContext context) { + final contentOrientation = findPositionForContent(offset); + final contentOffsetMultiplier = contentOrientation == "BELOW" ? 1.0 : -1.0; + isArrowUp = contentOffsetMultiplier == 1.0 ? true : false; + + final contentY = isArrowUp + ? position.getBottom() + (contentOffsetMultiplier * 3) + : position.getTop() + (contentOffsetMultiplier * 3); + + final contentFractionalOffset = contentOffsetMultiplier.clamp(-1.0, 0.0); + + double paddingTop = isArrowUp ? 22 : 0; + double paddingBottom = isArrowUp ? 0 : 27; + + if (!showArrow) { + paddingTop = 10; + paddingBottom = 10; + } + + if (container == null) { + return Stack( + children: [ + showArrow ? _getArrow(contentOffsetMultiplier) : Container(), + Positioned( + top: contentY, + left: _getLeft(), + right: _getRight(), + child: FractionalTranslation( + translation: Offset(0.0, contentFractionalOffset), + child: SlideTransition( + position: Tween( + begin: Offset(0.0, contentFractionalOffset / 10), + end: Offset(0.0, 0.100), + ).animate(animationOffset), + child: Material( + color: Colors.transparent, + child: Container( + padding: + EdgeInsets.only(top: paddingTop, bottom: paddingBottom), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GestureDetector( + onTap: onTooltipTap, + child: Container( + width: _getTooltipWidth(), + padding: EdgeInsets.symmetric(vertical: 8), + color: tooltipColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + child: Column( + crossAxisAlignment: title != null + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + title != null + ? Row( + children: [ + Padding( + padding: + const EdgeInsets.all(8.0), + child: Icon( + DoctorApp.search_patient), + ), + AppText( + title, + color: textColor, + margin: 2, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ], + ) + : Container(), + AppText( + description, + color: textColor, + margin: 8, + ), + ], + ), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ), + ) + ], + ); + } else { + return Stack( + children: [ + Positioned( + left: _getSpace(), + top: contentY - 10, + child: FractionalTranslation( + translation: Offset(0.0, contentFractionalOffset), + child: SlideTransition( + position: Tween( + begin: Offset(0.0, contentFractionalOffset / 5), + end: Offset(0.0, 0.100), + ).animate(animationOffset), + child: Material( + color: Colors.transparent, + child: GestureDetector( + onTap: onTooltipTap, + child: Container( + padding: EdgeInsets.only( + top: paddingTop, + ), + color: Colors.transparent, + child: Center( + child: container, + ), + ), + ), + ), + ), + ), + ), + ], + ); + } + } + + Widget _getArrow(contentOffsetMultiplier) { + final contentFractionalOffset = contentOffsetMultiplier.clamp(-1.0, 0.0); + return Positioned( + top: isArrowUp ? position.getBottom() : position.getTop() - 1, + left: position.getCenter() - 24, + child: FractionalTranslation( + translation: Offset(0.0, contentFractionalOffset), + child: SlideTransition( + position: Tween( + begin: Offset(0.0, contentFractionalOffset / 5), + end: Offset(0.0, 0.150), + ).animate(animationOffset), + child: isArrowUp + ? Icon( + Icons.arrow_drop_up, + color: tooltipColor, + size: 50, + ) + : Icon( + Icons.arrow_drop_down, + color: tooltipColor, + size: 50, + ), + ), + ), + ); + } +}