diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5be82..a19f98a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [3.0.2] + +* Migrate to `3.0.0`. + +## [3.0.1] + +* Change `Intro.of(context)?.start()` to `Intro.of(context).start()`. + +## [3.0.0] + +* Completely rewritten, please refer to example for usage. + ## [2.3.1] * Throw a friendly error when something goes wrong. diff --git a/README.md b/README.md index 9eff5de..cba3815 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,17 @@ A better way for new feature introduction and step-by-step users guide for your Flutter project. -# Since I no longer work at Tal, the repository has been moved to [https://github.com/minaxorg/flutter_intro](https://github.com/minaxorg/flutter_intro) +## Since I no longer work at Tal, the repository has been moved from [https://github.com/tal-tech/flutter_intro](https://github.com/tal-tech/flutter_intro) to here. - +## This is `3.0.0` version, if you find `2.x` documentation, [click here](./README_V2.md). + +I completely rewritten the 3.0 version, and the usage is clearer and more concise. + + Automatically adapt when the device screen orientation is switched. - + ## Usage @@ -18,159 +22,121 @@ To use this package, add `flutter_intro` as a [dependency in your pubspec.yaml f ### Init +Wrap the app root widget with `Intro`, also you can set some global properties on `Intro`. + ```dart import 'package:flutter_intro/flutter_intro.dart'; -Intro intro = Intro( - /// You can set it true to disable animation - noAnimation: false, - - /// The total number of guide pages, must be passed - stepCount: 4, - - /// Click on whether the mask is allowed to be closed. - maskClosable: true, - - /// When highlight widget is tapped. - onHighlightWidgetTap: (introStatus) { - print(introStatus); - }, - +Intro( /// The padding of the highlighted area and the widget - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), /// Border radius of the highlighted area borderRadius: BorderRadius.all(Radius.circular(4)), - /// Use the default useDefaultTheme provided by the library to quickly build a guide page - /// Need to customize the style and content of the guide page, implement the widgetBuilder method yourself - /// * Above version 2.3.0, you can use useAdvancedTheme to have more control over the style of the widget - /// * Please see https://github.com/tal-tech/flutter_intro/issues/26 - widgetBuilder: StepWidgetBuilder.useDefaultTheme( - /// Guide page text - texts: [ - 'Hello, I\'m Flutter Intro.', - 'I can help you quickly implement the Step By Step guide in the Flutter project.', - 'My usage is also very simple, you can quickly learn and use it through example and api documentation.', - 'In order to quickly implement the guidance, I also provide a set of out-of-the-box themes, I wish you all a happy use, goodbye!', - ], - /// Button text - buttonTextBuilder: (curr, total) { - return curr < total - 1 ? 'Next' : 'Finish'; - }, - ), -); -``` - + /// The mask color of step page + maskColor: const Color.fromRGBO(0, 0, 0, .6); -### Bind globalKey to widgets that need to be guided + /// No animation + noAnimation: false; -The `intro` object in the first step contains the `keys` property, and `keys` is a `globalKey` array of length `stepCount`. Just bind the `globalKey` in the array to the corresponding component. + /// Click on whether the mask is allowed to be closed. + maskClosable: false; -```dart -Placeholder( - /// the first guide page is the first item in the binding keys - key: intro.keys[0] + /// Custom button text + buttonTextBuilder: (order) => + order == 3 ? 'Custom Button Text' : 'Next', + + child: const YourApp(), ) ``` -### Run +### Add guided widget -That's it! +This time, the `IntroStepBuilder` class is added to do this, which solves the problem that the previous version could not dynamically add a guide. ```dart -intro.start(context); +IntroStepBuilder( + /// Guide order, can not be repeated with other + order: 1, + /// At least one of text and overlayBuilder + /// Use text to quickly add leading text + text: 'guide text', + /// Using overlayBuilder can be more customized, please refer to advanced usage in example + overlayBuilder: (params) { + /// + } + /// You can specify configuration for individual guide here + borderRadius: const BorderRadius.all(Radius.circular(64)), + builder: (context, key) => NeedGuideWidget( + /// You should bind key here. + key: key, + ), +) ``` -## Custom widgetBuilder method + + +### Run -If you need to completely customize the style and content of the guide page, you need to implement the `widgetBuilder` method yourself. +That's it! ```dart -final Widget Function(StepWidgetParams params) widgetBuilder; +Intro.of(context).start(); ``` -This method will be called internally by `flutter_intro` when the intro page appears, -and will pass some data on the current page in the form of parameters `StepWidgetParams`, -and finally render the component returned by this method on the screen. +## Advanced Usage ```dart -class StepWidgetParams { - /// Return to the previous guide page method, or null if there is none - final VoidCallback onPrev; - - /// Enter the next guide page method, or null if there is no - final VoidCallback onNext; - - /// End all guide page methods - final VoidCallback onFinish; - - /// Which guide page is currently displayed, starting from 0 - final int currentStepIndex; - - /// Total number of guide pages - final int stepCount; - - /// The width and height of the screen - final Size screenSize; - - /// The width and height of the highlighted component - final Size size; - - /// The coordinates of the upper left corner of the highlighted component - final Offset offset; -} +IntroStepBuilder( + ..., + overlayBuilder: (StepWidgetParams params) { + return YourOverlay(); + }, +) ``` - + -`StepWidgetParams` provides all the parameters needed to generate the guide page. -The theme provided by default is also based on this parameter to generate the guide page. +`StepWidgetParams` provides all the parameters needed to generate the guide overlay. ## Troubleshoot Q1. What if the highlighted area is not displayed completely? - + A1. That's because Intro provides 8px padding by default. - + We can change it by setting the value of padding. ```dart -intro = Intro( +Intro( ..., /// Set it to zero padding: EdgeInsets.zero, + child: const YourApp(), ); ``` - +
Q2. Can I set different configurations for each step? -A2. Above version `0.4.x`, you can set single or multiple step settings(padding & borderRadius) through setStepConfig and setStepsConfig. - +A2. Yes, you can set in every `IntroStepBuilder`. ```dart -intro.setStepConfig( - 1, - padding: EdgeInsets.symmetric( +IntroStepBuilder( + ..., + padding: const EdgeInsets.symmetric( vertical: -5, - horizontal: -8, - ), -); - -intro.setStepsConfig( - [0, 1], - borderRadius: BorderRadius.all( - Radius.circular( - 16, - ), + horizontal: -5, ), -); + borderRadius: const BorderRadius.all(Radius.circular(64)), + builder: (context, key) => YourWidget(), +) ```
@@ -180,32 +146,30 @@ Q3. Can I make the highlight area smaller? A3. You can do it by setting padding to a negative number. ```dart -intro.setStepConfig( - 1, - padding: EdgeInsets.symmetric( - vertical: -10, - horizontal: -8, +IntroStepBuilder( + ..., + padding: const EdgeInsets.symmetric( + vertical: -5, + horizontal: -5, ), -); + builder: (context, key) => YourWidget(), +) ``` - +
Q4. How can I manually destroy the guide page, such as the user pressing the back button? -A4. Above version `0.5.x`, you can call the dispose method of the intro instance. - -Notice: You can call the getStatus method only above version `2.1.0`. +A4. You can call the dispose method of the intro instance. ```dart WillPopScope( child: Scaffold(...), onWillPop: () async { - // sometimes you need get current status to make some judgements - IntroStatus introStatus = intro.getStatus(); - if (introStatus.isOpen) { - // destroy guide page when tap back key + Intro intro = Intro.of(context); + + if (intro.status.isOpen == true) { intro.dispose(); return false; } @@ -214,14 +178,6 @@ WillPopScope( ) ``` -
- -Q5: How to use in the web environment? - -A5: Due to [this bug](https://github.com/flutter/flutter/issues/69849) in Flutter, it is temporarily not supported for use on the Web.(Update: It works in Flutter 2.0+) - - - ## Example Please check the example in `example/lib/main.dart`. diff --git a/README_V2.md b/README_V2.md new file mode 100644 index 0000000..86c89f8 --- /dev/null +++ b/README_V2.md @@ -0,0 +1,226 @@ +# flutter_intro + +[![pub package](https://img.shields.io/pub/v/flutter_intro.svg)](https://pub.dartlang.org/packages/flutter_intro) + +A better way for new feature introduction and step-by-step users guide for your Flutter project. + + + +Automatically adapt when the device screen orientation is switched. + + + +## Usage + +To use this package, add `flutter_intro` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/packages-and-plugins/using-packages). + +### Init + +```dart +import 'package:flutter_intro/flutter_intro.dart'; + +Intro intro = Intro( + /// You can set it true to disable animation + noAnimation: false, + + /// The total number of guide pages, must be passed + stepCount: 4, + + /// Click on whether the mask is allowed to be closed. + maskClosable: true, + + /// When highlight widget is tapped. + onHighlightWidgetTap: (introStatus) { + print(introStatus); + }, + + /// The padding of the highlighted area and the widget + padding: EdgeInsets.all(8), + + /// Border radius of the highlighted area + borderRadius: BorderRadius.all(Radius.circular(4)), + + /// Use the default useDefaultTheme provided by the library to quickly build a guide page + /// Need to customize the style and content of the guide page, implement the widgetBuilder method yourself + /// * Above version 2.3.0, you can use useAdvancedTheme to have more control over the style of the widget + /// * Please see https://github.com/minaxorg/flutter_intro/issues/26 + widgetBuilder: StepWidgetBuilder.useDefaultTheme( + /// Guide page text + texts: [ + 'Hello, I\'m Flutter Intro.', + 'I can help you quickly implement the Step By Step guide in the Flutter project.', + 'My usage is also very simple, you can quickly learn and use it through example and api documentation.', + 'In order to quickly implement the guidance, I also provide a set of out-of-the-box themes, I wish you all a happy use, goodbye!', + ], + /// Button text + buttonTextBuilder: (curr, total) { + return curr < total - 1 ? 'Next' : 'Finish'; + }, + ), +); +``` + + +### Bind globalKey to widgets that need to be guided + +The `intro` object in the first step contains the `keys` property, and `keys` is a `globalKey` array of length `stepCount`. Just bind the `globalKey` in the array to the corresponding component. + +```dart +Placeholder( + /// the first guide page is the first item in the binding keys + key: intro.keys[0] +) +``` + +### Run + +That's it! + +```dart +intro.start(context); +``` + +## Custom widgetBuilder method + +If you need to completely customize the style and content of the guide page, you need to implement the `widgetBuilder` method yourself. + +```dart +final Widget Function(StepWidgetParams params) widgetBuilder; +``` + +This method will be called internally by `flutter_intro` when the intro page appears, +and will pass some data on the current page in the form of parameters `StepWidgetParams`, +and finally render the component returned by this method on the screen. + +```dart +class StepWidgetParams { + /// Return to the previous guide page method, or null if there is none + final VoidCallback onPrev; + + /// Enter the next guide page method, or null if there is no + final VoidCallback onNext; + + /// End all guide page methods + final VoidCallback onFinish; + + /// Which guide page is currently displayed, starting from 0 + final int currentStepIndex; + + /// Total number of guide pages + final int stepCount; + + /// The width and height of the screen + final Size screenSize; + + /// The width and height of the highlighted component + final Size size; + + /// The coordinates of the upper left corner of the highlighted component + final Offset offset; +} +``` + + + +`StepWidgetParams` provides all the parameters needed to generate the guide page. +The theme provided by default is also based on this parameter to generate the guide page. + +## Troubleshoot + +Q1. What if the highlighted area is not displayed completely? + + + +A1. That's because Intro provides 8px padding by default. + + + +We can change it by setting the value of padding. + +```dart +intro = Intro( + ..., + /// Set it to zero + padding: EdgeInsets.zero, +); +``` + + +
+ +Q2. Can I set different configurations for each step? + +A2. Above version `0.4.x`, you can set single or multiple step settings(padding & borderRadius) through setStepConfig and setStepsConfig. + +```dart +intro.setStepConfig( + 1, + padding: EdgeInsets.symmetric( + vertical: -5, + horizontal: -8, + ), +); + +intro.setStepsConfig( + [0, 1], + borderRadius: BorderRadius.all( + Radius.circular( + 16, + ), + ), +); +``` + +
+ +Q3. Can I make the highlight area smaller? + +A3. You can do it by setting padding to a negative number. + +```dart +intro.setStepConfig( + 1, + padding: EdgeInsets.symmetric( + vertical: -10, + horizontal: -8, + ), +); +``` + + +
+ +Q4. How can I manually destroy the guide page, such as the user pressing the back button? + +A4. Above version `0.5.x`, you can call the dispose method of the intro instance. + +Notice: You can call the getStatus method only above version `2.1.0`. + +```dart +WillPopScope( + child: Scaffold(...), + onWillPop: () async { + // sometimes you need get current status to make some judgements + IntroStatus introStatus = intro.getStatus(); + if (introStatus.isOpen) { + // destroy guide page when tap back key + intro.dispose(); + return false; + } + return true; + }, +) +``` + +
+ +Q5: How to use in the web environment? + +A5: Due to [this bug](https://github.com/flutter/flutter/issues/69849) in Flutter, it is temporarily not supported for use on the Web.(Update: It works in Flutter 2.0+) + + + +## Example + +Please check the example in `example/lib/main.dart`. + diff --git a/doc/v3/example1.gif b/doc/v3/example1.gif new file mode 100644 index 0000000..0e5667e Binary files /dev/null and b/doc/v3/example1.gif differ diff --git a/doc/v3/img1.png b/doc/v3/img1.png new file mode 100644 index 0000000..2ed32d5 Binary files /dev/null and b/doc/v3/img1.png differ diff --git a/example/lib/advanced_usage.dart b/example/lib/advanced_usage.dart new file mode 100644 index 0000000..4afa5c6 --- /dev/null +++ b/example/lib/advanced_usage.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; + +class AdvancedUsage extends StatefulWidget { + const AdvancedUsage({Key? key}) : super(key: key); + + @override + State createState() => _AdvancedUsageState(); +} + +class _AdvancedUsageState extends State { + bool rendered = false; + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + Intro intro = Intro.of(context); + + if (intro.status.isOpen == true) { + intro.dispose(); + return false; + } + return true; + }, + child: Scaffold( + appBar: AppBar(), + body: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntroStepBuilder( + /// 2nd guide + order: 2, + overlayBuilder: (params) { + return Container( + color: Colors.teal, + padding: const EdgeInsets.all(16), + child: Column( + children: [ + params.onNext == null + ? Column( + children: const [ + Text( + 'Of course, you can also render what you want through overlayBuilder.', + style: TextStyle(height: 1.6), + ), + Text( + 'In addition, we can finally add new guide widget dynamically.', + style: TextStyle(height: 1.6), + ), + Text( + 'Click highlight area to add new widget.', + style: TextStyle(height: 1.6), + ) + ], + ) + : const Text( + 'As you can see, you can move on to the next step'), + Padding( + padding: const EdgeInsets.only( + top: 16, + ), + child: Row( + children: [ + IntroButton( + text: 'Prev', + onPressed: params.onPrev, + ), + IntroButton( + text: 'Next', + onPressed: params.onNext, + ), + ], + ), + ), + ], + ), + ); + }, + onHighlightWidgetTap: () { + setState(() { + rendered = true; + }); + }, + builder: (context, key) => Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Placeholder( + key: key, + fallbackWidth: 100, + fallbackHeight: 100, + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + rendered + ? IntroStepBuilder( + order: 3, + onWidgetLoad: () { + Intro.of(context).refresh(); + }, + overlayBuilder: (params) { + return Container( + padding: const EdgeInsets.all(16), + color: Colors.teal, + child: Column( + children: [ + const Text( + 'That\'s it, hopefully version 3.0 makes you feel better than 2.0', + ), + Padding( + padding: const EdgeInsets.only( + top: 16, + ), + child: Row( + children: [ + IntroButton( + onPressed: params.onPrev, + text: 'Prev', + ), + IntroButton( + onPressed: params.onNext, + text: 'Next', + ), + IntroButton( + onPressed: params.onFinish, + text: 'Finish', + ), + ], + ), + ), + ], + ), + ); + }, + builder: (context, key) => Text( + 'I am a delay render widget.', + key: key, + ), + ) + : const SizedBox.shrink(), + ], + ), + ), + ), + floatingActionButton: IntroStepBuilder( + /// 1st guide + order: 1, + text: + 'Some properties on IntroStepBuilder like `borderRadius` `padding`' + ' allow you to configure the configuration of this step.', + padding: const EdgeInsets.symmetric( + vertical: -5, + horizontal: -5, + ), + borderRadius: const BorderRadius.all(Radius.circular(64)), + builder: (context, key) => FloatingActionButton( + key: key, + child: const Icon( + Icons.play_arrow, + ), + onPressed: () { + Intro.of(context).start(); + }, + ), + ), + ), + ); + } +} diff --git a/example/lib/demo_usage.dart b/example/lib/demo_usage.dart new file mode 100644 index 0000000..82e8588 --- /dev/null +++ b/example/lib/demo_usage.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; + +class DemoUsage extends StatefulWidget { + const DemoUsage({Key? key}) : super(key: key); + + @override + State createState() => _DemoUsageState(); +} + +class _DemoUsageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: IntroStepBuilder( + order: 2, + text: + 'I can help you quickly implement the Step By Step guide in the Flutter project.', + builder: (context, key) => Placeholder( + key: key, + fallbackHeight: 100, + ), + ), + ), + const SizedBox( + height: 16, + ), + IntroStepBuilder( + order: 3, + text: + 'My usage is also very simple, you can quickly learn and use it through example and api documentation.', + builder: (context, key) => Placeholder( + key: key, + fallbackHeight: 100, + ), + ), + ], + ), + ), + bottomNavigationBar: BottomNavigationBar( + items: [ + BottomNavigationBarItem( + label: 'Home', + icon: IntroStepBuilder( + order: 1, + text: 'Welcome to flutter_intro', + padding: const EdgeInsets.only( + bottom: 20, + left: 16, + right: 16, + top: 8, + ), + onWidgetLoad: () { + Intro.of(context).start(); + }, + builder: (context, key) => Icon( + Icons.home, + key: key, + ), + ), + ), + const BottomNavigationBarItem( + label: 'Book', + icon: Icon(Icons.book), + ), + const BottomNavigationBarItem( + label: 'School', + icon: Icon(Icons.school), + ), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index e5cdb2e..963bb45 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,8 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; +import 'package:intro/advanced_usage.dart'; +import 'package:intro/demo_usage.dart'; +import 'package:intro/simple_usage.dart'; void main() { runApp(const MyApp()); @@ -36,48 +37,45 @@ class StartPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ElevatedButton( - child: const Text('Start with useDefaultTheme'), onPressed: () { Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => const MyHomePage( - title: 'Flutter Intro', - mode: Mode.defaultTheme, + builder: (BuildContext context) => Intro( + padding: EdgeInsets.zero, + borderRadius: const BorderRadius.all(Radius.circular(4)), + maskColor: const Color.fromRGBO(0, 0, 0, .6), + child: const DemoUsage(), ), ), ); }, - ), - const SizedBox( - height: 16, + child: const Text('Demo'), ), ElevatedButton( - child: const Text('Start with useAdvancedTheme'), onPressed: () { Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => const MyHomePage( - title: 'Flutter Intro', - mode: Mode.advancedTheme, + builder: (BuildContext context) => Intro( + buttonTextBuilder: (order) => + order == 3 ? 'Custom Button Text' : 'Next', + child: const SimpleUsage(), ), ), ); }, - ), - const SizedBox( - height: 16, + child: const Text('Simple Usage'), ), ElevatedButton( - child: const Text('Start with customTheme'), + child: const Text('Advanced Usage'), onPressed: () { Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => const MyHomePage( - title: 'Flutter Intro', - mode: Mode.customTheme, + builder: (BuildContext context) => Intro( + maskClosable: true, + child: const AdvancedUsage(), ), ), ); @@ -89,263 +87,3 @@ class StartPage extends StatelessWidget { ); } } - -enum Mode { - defaultTheme, - customTheme, - advancedTheme, -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({ - Key? key, - required this.title, - required this.mode, - }) : super(key: key); - - final String title; - - final Mode mode; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - late Intro intro; - - Widget customThemeWidgetBuilder(StepWidgetParams stepWidgetParams) { - List texts = [ - 'Hello, I\'m Flutter Intro.', - 'I can help you quickly implement the Step By Step guide in the Flutter project.', - 'My usage is also very simple, you can quickly learn and use it through example and api documentation.', - 'In order to quickly implement the guidance, I also provide a set of out-of-the-box themes, I wish you all a happy use, goodbye!', - ]; - return Padding( - padding: const EdgeInsets.all( - 32, - ), - child: Column( - children: [ - const SizedBox( - height: 40, - ), - Text( - '${texts[stepWidgetParams.currentStepIndex]}【${stepWidgetParams.currentStepIndex + 1} / ${stepWidgetParams.stepCount}】', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - Row( - children: [ - ElevatedButton( - onPressed: stepWidgetParams.onPrev, - child: const Text( - 'Prev', - ), - ), - const SizedBox( - width: 16, - ), - ElevatedButton( - onPressed: stepWidgetParams.onNext, - child: const Text( - 'Next', - ), - ), - const SizedBox( - width: 16, - ), - ElevatedButton( - onPressed: stepWidgetParams.onFinish, - child: const Text( - 'Finish', - ), - ), - ], - ), - ], - ), - ); - } - - @override - void initState() { - super.initState(); - if (widget.mode == Mode.defaultTheme) { - /// init Intro - intro = Intro( - stepCount: 4, - maskClosable: true, - onHighlightWidgetTap: (introStatus) { - print(introStatus); - }, - - /// use defaultTheme - widgetBuilder: StepWidgetBuilder.useDefaultTheme( - texts: [ - 'Hello, I\'m Flutter Intro.', - 'I can help you quickly implement the Step By Step guide in the Flutter project.', - 'My usage is also very simple, you can quickly learn and use it through example and api documentation.', - 'In order to quickly implement the guidance, I also provide a set of out-of-the-box themes, I wish you all a happy use, goodbye!', - ], - buttonTextBuilder: (currPage, totalPage) { - return currPage < totalPage - 1 ? 'Next' : 'Finish'; - }, - ), - ); - intro.setStepConfig( - 0, - borderRadius: BorderRadius.circular(64), - ); - } - if (widget.mode == Mode.advancedTheme) { - /// init Intro - intro = Intro( - stepCount: 4, - maskClosable: false, - onHighlightWidgetTap: (introStatus) { - print(introStatus); - }, - - /// useAdvancedTheme - widgetBuilder: StepWidgetBuilder.useAdvancedTheme( - widgetBuilder: (params) { - return Container( - decoration: BoxDecoration( - color: Colors.red.withOpacity(.6), - ), - child: Column( - children: [ - Text( - '${params.currentStepIndex + 1}/${params.stepCount}', - style: const TextStyle( - color: Colors.green, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Row( - children: [ - ElevatedButton( - onPressed: params.onPrev, - child: const Text('Prev'), - ), - ElevatedButton( - onPressed: params.onNext, - child: const Text('Next'), - ), - ElevatedButton( - onPressed: params.onFinish, - child: const Text('Finish'), - ), - ], - ), - ], - ), - ); - }, - ), - ); - intro.setStepConfig( - 0, - borderRadius: BorderRadius.circular(64), - ); - } - if (widget.mode == Mode.customTheme) { - /// init Intro - intro = Intro( - stepCount: 4, - - maskClosable: true, - - /// implement widgetBuilder function - widgetBuilder: customThemeWidgetBuilder, - ); - } - - Timer( - const Duration( - milliseconds: 500, - ), - () { - /// start the intro - intro.start(context); - }, - ); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - child: Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: SingleChildScrollView( - child: Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 100, - child: Placeholder( - /// 2nd guide - key: intro.keys[1], - fallbackHeight: 100, - ), - ), - const SizedBox( - height: 16, - ), - Placeholder( - /// 3rd guide - key: intro.keys[2], - fallbackHeight: 100, - ), - const SizedBox( - height: 16, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - width: 100, - child: Placeholder( - /// 4th guide - key: intro.keys[3], - fallbackHeight: 100, - ), - ), - ], - ), - ], - ), - ), - ), - floatingActionButton: FloatingActionButton( - /// 1st guide - key: intro.keys[0], - child: const Icon( - Icons.play_arrow, - ), - onPressed: () { - intro.start(context); - }, - ), - ), - onWillPop: () async { - // sometimes you need get current status - IntroStatus introStatus = intro.getStatus(); - if (introStatus.isOpen) { - // destroy guide page when tap back key - intro.dispose(); - return false; - } - return true; - }, - ); - } -} diff --git a/example/lib/simple_usage.dart b/example/lib/simple_usage.dart new file mode 100644 index 0000000..c9d259d --- /dev/null +++ b/example/lib/simple_usage.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; + +class SimpleUsage extends StatelessWidget { + const SimpleUsage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return WillPopScope( + child: Scaffold( + appBar: AppBar(), + body: Padding( + padding: const EdgeInsets.all( + 16, + ), + child: Center( + child: Column( + children: [ + IntroStepBuilder( + order: 2, + text: + 'Use IntroStepBuilder to wrap the widget you need to guide.' + ' Add the necessary order to it, and then add the key in the builder method to the widget.', + builder: (context, key) => Text( + 'Tap the floatingActionButton to start.', + key: key, + ), + ), + const SizedBox( + height: 16, + ), + IntroStepBuilder( + order: 3, + text: + 'If you need more configuration, please refer to Advanced Usage.', + builder: (context, key) => Text( + 'And you can use `buttonTextBuilder` to set the button text.', + key: key, + ), + ), + ], + ), + ), + ), + floatingActionButton: IntroStepBuilder( + order: 1, + text: 'OK, let\'s start.', + builder: (context, key) => FloatingActionButton( + key: key, + child: const Icon( + Icons.play_arrow, + ), + onPressed: () { + Intro.of(context).start(); + }, + ), + ), + ), + onWillPop: () async { + Intro intro = Intro.of(context); + + if (intro.status.isOpen == true) { + intro.dispose(); + return false; + } + return true; + }, + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 165f9eb..512e786 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -42,7 +42,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" cupertino_icons: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -68,14 +68,14 @@ packages: path: ".." relative: true source: path - version: "2.3.1" + version: "3.0.2" flutter_lints: dependency: "direct dev" description: name: flutter_lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -87,7 +87,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" matcher: dependency: transitive description: @@ -101,7 +101,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -115,7 +115,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -127,7 +127,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -162,21 +162,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" sdks: - dart: ">=2.16.1 <3.0.0" + dart: ">=2.17.0-206.0.dev <3.0.0" flutter: ">=1.17.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cee5937..d32fef6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -47,7 +47,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/lib/flutter_intro.dart b/lib/flutter_intro.dart index 83b4c93..1bfbb85 100644 --- a/lib/flutter_intro.dart +++ b/lib/flutter_intro.dart @@ -7,163 +7,111 @@ import 'package:flutter/material.dart'; part 'delay_rendered_widget.dart'; part 'flutter_intro_exception.dart'; +part 'intro_button.dart'; part 'intro_status.dart'; +part 'intro_step_builder.dart'; +part 'overlay_position.dart'; part 'step_widget_builder.dart'; part 'step_widget_params.dart'; part 'throttling.dart'; -/// Flutter Intro main class -/// -/// Pass in [stepCount] when instantiating [Intro] object, and [widgetBuilder] -/// Obtain [GlobalKey] from [Intro.keys] and add it to the [Widget] where you need to add a guide page -/// Finally execute the [start] method, the parameter is the current [BuildContext], you can -/// -/// {@tool snippet} -/// ```dart -/// final Intro intro = Intro( -/// stepCount: 4, -/// widgetBuilder: widgetBuilder, -/// ); -/// -/// Container( -/// key: intro.keys[0], -/// ); -/// Text( -/// 'need focus widget', -/// key: intro.keys[1], -/// ); -/// -/// intro.start(context); -/// ``` -/// {@end-tool} -/// -class Intro { - bool _removed = false; - double? _widgetWidth; - double? _widgetHeight; - Offset? _widgetOffset; - OverlayEntry? _overlayEntry; - int _currentStepIndex = 0; - Widget? _stepWidget; - List _configMap = []; - List _globalKeys = []; - late Duration _animationDuration; - late Size _lastScreenSize; +class Intro extends InheritedWidget { + static BuildContext? _context; + static OverlayEntry? _overlayEntry; + static bool _removed = false; + static Size _screenSize = Size(0, 0); + static Widget _overlayWidget = SizedBox.shrink(); + static IntroStepBuilder? _currentIntroStepBuilder; + static Size _widgetSize = Size(0, 0); + static Offset _widgetOffset = Offset(0, 0); + final _th = _Throttling(duration: Duration(milliseconds: 500)); + final List _introStepBuilderList = []; + final List _finishedIntroStepBuilderList = []; + late final Duration _animationDuration; + + /// [Widget] [padding] of the selected area, the default is [EdgeInsets.all(8)] + final EdgeInsets padding; + + /// [Widget] [borderRadius] of the selected area, the default is [BorderRadius.all(Radius.circular(4))] + final BorderRadiusGeometry borderRadius; /// The mask color of step page final Color maskColor; - /// Current step page index - /// 2021-03-31 @caden - /// I don’t remember why this parameter was exposed at the time, - /// it seems to be useless, and there is a bug in this one, so let’s block it temporarily. - // int get currentStepIndex => _currentStepIndex; - /// No animation final bool noAnimation; - // Click on whether the mask is allowed to be closed. + /// Click on whether the mask is allowed to be closed. final bool maskClosable; - /// The method of generating the content of the guide page, - /// which will be called internally by [Intro] when the guide page appears. - /// And will pass in some parameters on the current page through [StepWidgetParams] - final Widget Function(StepWidgetParams params) widgetBuilder; - - /// [Widget] [padding] of the selected area, the default is [EdgeInsets.all(8)] - final EdgeInsets padding; - - /// [Widget] [borderRadius] of the selected area, the default is [BorderRadius.all(Radius.circular(4))] - final BorderRadiusGeometry borderRadius; - - /// How many steps are there in total - final int stepCount; + /// [order] order + final String Function( + int order, + )? buttonTextBuilder; - /// The highlight widget tapped callback - final void Function(IntroStatus introStatus)? onHighlightWidgetTap; - - /// Create an Intro instance, the parameter [stepCount] is the number of guide pages - /// [widgetBuilder] is the method of generating the guide page, and returns a [Widget] as the guide page Intro({ - required this.widgetBuilder, - required this.stepCount, + this.padding = const EdgeInsets.all(8), + this.borderRadius = const BorderRadius.all(Radius.circular(4)), this.maskColor = const Color.fromRGBO(0, 0, 0, .6), this.noAnimation = false, this.maskClosable = false, - this.borderRadius = const BorderRadius.all(Radius.circular(4)), - this.padding = const EdgeInsets.all(8), - this.onHighlightWidgetTap, - }) : assert(stepCount > 0) { + this.buttonTextBuilder, + required Widget child, + }) : super(child: child) { _animationDuration = noAnimation ? Duration(milliseconds: 0) : Duration(milliseconds: 300); - for (int i = 0; i < stepCount; i++) { - _globalKeys.add(GlobalKey()); - _configMap.add({}); - } } - List get keys => _globalKeys; - - /// Set the configuration of the specified number of steps - /// - /// [stepIndex] Which step of configuration needs to be modified - /// [padding] Padding setting - /// [borderRadius] BorderRadius setting - void setStepConfig( - int stepIndex, { - EdgeInsets? padding, - BorderRadiusGeometry? borderRadius, - }) { - assert(stepIndex >= 0 && stepIndex < stepCount); - _configMap[stepIndex] = { - 'padding': padding, - 'borderRadius': borderRadius, - }; - } + IntroStatus get status => IntroStatus(isOpen: _overlayEntry != null); + + bool get hasNextStep => + _currentIntroStepBuilder == null || + _introStepBuilderList.where( + (element) { + return element.order > _currentIntroStepBuilder!.order; + }, + ).length > + 0; + + bool get hasPrevStep => + _finishedIntroStepBuilderList + .indexWhere((element) => element == _currentIntroStepBuilder) > + 0; - /// Set the configuration of multiple steps - /// - /// [stepsIndex] Which steps of configuration needs to be modified - /// [padding] Padding setting - /// [borderRadius] BorderRadius setting - void setStepsConfig( - List stepsIndex, { - EdgeInsets? padding, - BorderRadiusGeometry? borderRadius, + IntroStepBuilder? _getNextIntroStepBuilder({ + bool isUpdate = false, }) { - assert(stepsIndex - .every((stepIndex) => stepIndex >= 0 && stepIndex < stepCount)); - stepsIndex.forEach((index) { - setStepConfig( - index, - padding: padding, - borderRadius: borderRadius, - ); - }); + if (isUpdate) { + return _currentIntroStepBuilder; + } + int index = _finishedIntroStepBuilderList + .indexWhere((element) => element == _currentIntroStepBuilder); + if (index != _finishedIntroStepBuilderList.length - 1) { + return _finishedIntroStepBuilderList[index + 1]; + } else { + _introStepBuilderList.sort((a, b) => a.order - b.order); + final introStepBuilder = + _introStepBuilderList.cast().firstWhere( + (e) => !_finishedIntroStepBuilderList.contains(e), + orElse: () => null, + ); + return introStepBuilder; + } } - void _getWidgetInfo(GlobalKey globalKey) { - if (globalKey.currentContext == null) { - throw FlutterIntroException( - 'The current context is null, because there is no widget in the tree that matches this global key.' - ' Please check whether the globalKey in intro.keys has forgotten to bind.', - ); + IntroStepBuilder? _getPrevIntroStepBuilder({ + bool isUpdate = false, + }) { + if (isUpdate) { + return _currentIntroStepBuilder; } - - EdgeInsets? currentConfig = _configMap[_currentStepIndex]['padding']; - RenderBox renderBox = - globalKey.currentContext!.findRenderObject() as RenderBox; - _widgetWidth = renderBox.size.width + - (currentConfig?.horizontal ?? padding.horizontal); - _widgetHeight = - renderBox.size.height + (currentConfig?.vertical ?? padding.vertical); - _widgetOffset = Offset( - renderBox.localToGlobal(Offset.zero).dx - - (currentConfig?.left ?? padding.left), - renderBox.localToGlobal(Offset.zero).dy - - (currentConfig?.top ?? padding.top), - ); + int index = _finishedIntroStepBuilderList + .indexWhere((element) => element == _currentIntroStepBuilder); + if (index > 0) { + return _finishedIntroStepBuilderList[index - 1]; + } + return null; } Widget _widgetBuilder({ @@ -203,20 +151,165 @@ class Intro { ); } - void _showOverlay( - BuildContext context, - GlobalKey globalKey, - ) { + void _onFinish() { + if (_overlayEntry == null) return; + + _removed = true; + _overlayEntry!.markNeedsBuild(); + Timer(_animationDuration, () { + if (_overlayEntry == null) return; + _overlayEntry!.remove(); + _removed = false; + _overlayEntry = null; + _introStepBuilderList.clear(); + _finishedIntroStepBuilderList.clear(); + }); + } + + void _render({ + bool isUpdate = false, + bool reverse = false, + }) { + IntroStepBuilder? introStepBuilder = reverse + ? _getPrevIntroStepBuilder( + isUpdate: isUpdate, + ) + : _getNextIntroStepBuilder( + isUpdate: isUpdate, + ); + _currentIntroStepBuilder = introStepBuilder; + + if (introStepBuilder == null) { + _onFinish(); + return; + } + + BuildContext? currentContext = introStepBuilder._key.currentContext; + + if (currentContext == null) { + throw FlutterIntroException( + 'The current context is null, because there is no widget in the tree that matches this global key.' + ' Please check whether the key in IntroStepBuilder(order: ${introStepBuilder.order}) has forgotten to bind.' + ' If you are already bound, it means you have encountered a bug, please let me know.', + ); + } + + RenderBox renderBox = currentContext.findRenderObject() as RenderBox; + + _screenSize = MediaQuery.of(_context!).size; + _widgetSize = Size( + renderBox.size.width + + (introStepBuilder.padding?.horizontal ?? padding.horizontal), + renderBox.size.height + + (introStepBuilder.padding?.vertical ?? padding.vertical), + ); + _widgetOffset = Offset( + renderBox.localToGlobal(Offset.zero).dx - + (introStepBuilder.padding?.left ?? padding.left), + renderBox.localToGlobal(Offset.zero).dy - + (introStepBuilder.padding?.top ?? padding.top), + ); + + OverlayPosition position = _StepWidgetBuilder.getOverlayPosition( + screenSize: _screenSize, + size: _widgetSize, + offset: _widgetOffset, + ); + + if (!_finishedIntroStepBuilderList.contains(introStepBuilder)) { + _finishedIntroStepBuilderList.add(introStepBuilder); + } + + if (introStepBuilder.overlayBuilder != null) { + _overlayWidget = Stack( + children: [ + Positioned( + child: SizedBox( + child: introStepBuilder.overlayBuilder!( + StepWidgetParams( + order: introStepBuilder.order, + onNext: hasNextStep ? _render : null, + onPrev: hasPrevStep + ? () { + _render(reverse: true); + } + : null, + onFinish: _onFinish, + screenSize: _screenSize, + size: _widgetSize, + offset: _widgetOffset, + ), + ), + ), + width: position.width, + left: position.left, + top: position.top, + bottom: position.bottom, + right: position.right, + ), + ], + ); + } else if (introStepBuilder.text != null) { + _overlayWidget = Stack( + children: [ + Positioned( + child: SizedBox( + width: position.width, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: position.crossAxisAlignment, + children: [ + Text( + introStepBuilder.text!, + softWrap: true, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.4, + color: Colors.white, + ), + ), + SizedBox( + height: 12, + ), + IntroButton( + text: buttonTextBuilder == null + ? 'Next' + : buttonTextBuilder!(introStepBuilder.order), + onPressed: _render, + ), + ], + ), + ), + left: position.left, + top: position.top, + bottom: position.bottom, + right: position.right, + ), + ], + ); + } + + if (_overlayEntry == null) { + _createOverlay(); + } else { + _overlayEntry!.markNeedsBuild(); + } + } + + void _createOverlay() { _overlayEntry = new OverlayEntry( builder: (BuildContext context) { - Size screenSize = MediaQuery.of(context).size; + Size currentScreenSize = MediaQuery.of(context).size; + + if (_screenSize.width != currentScreenSize.width || + _screenSize.height != currentScreenSize.height) { + _screenSize = currentScreenSize; - if (screenSize.width != _lastScreenSize.width || - screenSize.height != _lastScreenSize.height) { - _lastScreenSize = screenSize; _th.throttle(() { - _createStepWidget(context); - _overlayEntry!.markNeedsBuild(); + _render( + isUpdate: true, + ); }); } @@ -243,39 +336,31 @@ class Intro { bottom: 0, onTap: maskClosable ? () { - if (stepCount - 1 == _currentStepIndex) { - _onFinish(); - } else { - _onNext(context); - } + Future.delayed( + const Duration(milliseconds: 200), + () { + _render(); + }, + ); } : null, ), _widgetBuilder( - width: _widgetWidth, - height: _widgetHeight, - left: _widgetOffset!.dx, - top: _widgetOffset!.dy, - // Skipping through the intro very fast may cause currentStepIndex to out of bounds - // I have tried to fix it, here is just to make the code safer - // https://github.com/tal-tech/flutter_intro/issues/22 - borderRadiusGeometry: _currentStepIndex < stepCount - ? _configMap[_currentStepIndex]['borderRadius'] ?? - borderRadius - : borderRadius, - onTap: onHighlightWidgetTap != null - ? () { - IntroStatus introStatus = getStatus(); - onHighlightWidgetTap!(introStatus); - } - : null, + width: _widgetSize.width, + height: _widgetSize.height, + left: _widgetOffset.dx, + top: _widgetOffset.dy, + borderRadiusGeometry: + _currentIntroStepBuilder?.borderRadius ?? + borderRadius, + onTap: _currentIntroStepBuilder?.onHighlightWidgetTap, ), ], ), ), _DelayRenderedWidget( duration: _animationDuration, - child: _stepWidget, + child: _overlayWidget, ), ], ), @@ -283,90 +368,31 @@ class Intro { ); }, ); - Overlay.of(context)!.insert(_overlayEntry!); + Overlay.of(_context!)!.insert(_overlayEntry!); } - void _onNext(BuildContext context) { - if (_currentStepIndex + 1 < stepCount) { - _currentStepIndex++; - _renderStep(context); - } + void start() { + dispose(); + _render(); } - void _onPrev(BuildContext context) { - if (_currentStepIndex - 1 >= 0) { - _currentStepIndex--; - _renderStep(context); - } - } - - void _onFinish() { - if (_overlayEntry == null) return; - _removed = true; - _overlayEntry!.markNeedsBuild(); - Timer(_animationDuration, () { - if (_overlayEntry == null) return; - _overlayEntry!.remove(); - _overlayEntry = null; - }); - } - - void _createStepWidget(BuildContext context) { - _getWidgetInfo(_globalKeys[_currentStepIndex]); - Size screenSize = MediaQuery.of(context).size; - Size widgetSize = Size(_widgetWidth!, _widgetHeight!); - - _stepWidget = widgetBuilder(StepWidgetParams( - screenSize: screenSize, - size: widgetSize, - onNext: _currentStepIndex == stepCount - 1 - ? null - : () { - _onNext(context); - }, - onPrev: _currentStepIndex == 0 - ? null - : () { - _onPrev(context); - }, - offset: _widgetOffset, - currentStepIndex: _currentStepIndex, - stepCount: stepCount, - onFinish: _onFinish, - )); - } - - void _renderStep(BuildContext context) { - _createStepWidget(context); - _overlayEntry!.markNeedsBuild(); + void refresh() { + _render( + isUpdate: true, + ); } - /// Trigger the start method of the guided operation - /// - /// [context] Current environment [BuildContext] - void start(BuildContext context) { - _lastScreenSize = MediaQuery.of(context).size; - _removed = false; - _currentStepIndex = 0; - _createStepWidget(context); - _showOverlay( - context, - _globalKeys[_currentStepIndex], - ); + static Intro of(BuildContext context) { + _context = context; + return context.dependOnInheritedWidgetOfExactType()!; } - /// Destroy the guide page and release all resources void dispose() { _onFinish(); } - /// Get intro instance current status - IntroStatus getStatus() { - bool isOpen = _overlayEntry != null; - IntroStatus introStatus = IntroStatus( - isOpen: isOpen, - currentStepIndex: _currentStepIndex, - ); - return introStatus; + @override + bool updateShouldNotify(Intro oldWidget) { + return false; } } diff --git a/lib/intro_button.dart b/lib/intro_button.dart new file mode 100644 index 0000000..f83f40c --- /dev/null +++ b/lib/intro_button.dart @@ -0,0 +1,62 @@ +part of flutter_intro; + +class IntroButton extends StatelessWidget { + final VoidCallback? onPressed; + final String text; + const IntroButton({ + Key? key, + required this.text, + this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 28, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + primary: Colors.white, + shape: StadiumBorder(), + side: onPressed == null + ? null + : BorderSide( + color: Colors.white, + ), + padding: EdgeInsets.symmetric( + vertical: 0, + horizontal: 8, + ), + ), + // style: ButtonStyle( + // foregroundColor: MaterialStateProperty.all( + // Colors.white, + // ), + // overlayColor: MaterialStateProperty.all( + // Colors.white.withOpacity(0.1), + // ), + // side: MaterialStateProperty.all( + // BorderSide( + // color: Colors.white, + // ), + // ), + // padding: MaterialStateProperty.all( + // EdgeInsets.symmetric( + // vertical: 0, + // horizontal: 8, + // ), + // ), + // shape: MaterialStateProperty.all( + // StadiumBorder(), + // ), + // ), + onPressed: onPressed, + child: Text( + text, + style: TextStyle( + fontSize: 12, + ), + ), + ), + ); + } +} diff --git a/lib/intro_status.dart b/lib/intro_status.dart index 6fe76f7..9a53896 100644 --- a/lib/intro_status.dart +++ b/lib/intro_status.dart @@ -6,16 +6,12 @@ class IntroStatus { /// Flutter_intro is showing on the screen or not final bool isOpen; - /// Current step page index - final int currentStepIndex; - IntroStatus({ required this.isOpen, - required this.currentStepIndex, }); @override String toString() { - return 'IntroStatus(isOpen: $isOpen, currentStepIndex: $currentStepIndex)'; + return 'IntroStatus(isOpen: $isOpen)'; } } diff --git a/lib/intro_step_builder.dart b/lib/intro_step_builder.dart new file mode 100644 index 0000000..d8286ab --- /dev/null +++ b/lib/intro_step_builder.dart @@ -0,0 +1,82 @@ +part of flutter_intro; + +class IntroStepBuilder extends StatefulWidget { + final Widget Function( + BuildContext context, + GlobalKey key, + ) builder; + + ///Set the running order, the smaller the number, the first + final int order; + + /// The method of generating the content of the guide page, + /// which will be called internally by [Intro] when the guide page appears. + /// And will pass in some parameters on the current page through [StepWidgetParams] + final Widget Function(StepWidgetParams params)? overlayBuilder; + + /// When highlight widget is tapped + final VoidCallback? onHighlightWidgetTap; + + /// [Widget] [borderRadius] of the selected area, the default is [BorderRadius.all(Radius.circular(4))] + final BorderRadiusGeometry? borderRadius; + + /// [Widget] [padding] of the selected area, the default is [EdgeInsets.all(8)] + final EdgeInsets? padding; + + final String? text; + + /// When widget loaded (means the key is add to context) + final VoidCallback? onWidgetLoad; + + IntroStepBuilder({ + Key? key, + required this.order, + required this.builder, + this.text, + this.overlayBuilder, + this.borderRadius, + this.onHighlightWidgetTap, + this.padding, + this.onWidgetLoad, + }) : assert(text != null || overlayBuilder != null), + super(key: key); + + GlobalKey get _key => GlobalObjectKey(order); + + @override + State createState() => _IntroStepBuilderState(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'IntroStepBuilder(order: $order)'; + } + + @override + bool operator ==(Object other) { + return other is IntroStepBuilder && this.order == other.order; + } +} + +class _IntroStepBuilderState extends State { + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () { + Intro flutterIntro = Intro.of(context); + if (!flutterIntro._introStepBuilderList.contains(widget)) { + flutterIntro._introStepBuilderList.add(widget); + if (widget.onWidgetLoad != null) { + widget.onWidgetLoad!(); + } + } + }); + } + + @override + Widget build(BuildContext context) { + return widget.builder( + context, + widget._key, + ); + } +} diff --git a/lib/overlay_position.dart b/lib/overlay_position.dart new file mode 100644 index 0000000..2e1f557 --- /dev/null +++ b/lib/overlay_position.dart @@ -0,0 +1,19 @@ +part of flutter_intro; + +class OverlayPosition { + double? left; + double? right; + double? bottom; + double? top; + double width; + CrossAxisAlignment crossAxisAlignment; + + OverlayPosition({ + this.left, + this.right, + this.bottom, + this.top, + required this.width, + required this.crossAxisAlignment, + }); +} diff --git a/lib/step_widget_builder.dart b/lib/step_widget_builder.dart index b9f0d18..34bb4cb 100644 --- a/lib/step_widget_builder.dart +++ b/lib/step_widget_builder.dart @@ -16,16 +16,8 @@ part of flutter_intro; /// ``` /// {@end-tool} /// -class StepWidgetBuilder { - @visibleForTesting - static Map smartGetPosition({ - required Size size, - required Size screenSize, - required Offset offset, - }) => - _smartGetPosition(size: size, screenSize: screenSize, offset: offset); - - static Map _smartGetPosition({ +class _StepWidgetBuilder { + static OverlayPosition getOverlayPosition({ required Size size, required Size screenSize, required Offset offset, @@ -38,183 +30,45 @@ class StepWidgetBuilder { double topArea = screenHeight - height - bottomArea; double rightArea = screenWidth - offset.dx - width; double leftArea = screenWidth - width - rightArea; - Map position = Map(); - position['crossAxisAlignment'] = CrossAxisAlignment.start; + OverlayPosition position = OverlayPosition( + width: 0, + crossAxisAlignment: CrossAxisAlignment.start, + ); if (topArea > bottomArea) { - position['bottom'] = bottomArea + height + 16; + position.bottom = bottomArea + height + 16; } else { - position['top'] = offset.dy + height + 12; + position.top = offset.dy + height + 12; } if (leftArea > rightArea) { - position['right'] = rightArea <= 0 ? 16.0 : rightArea; - position['crossAxisAlignment'] = CrossAxisAlignment.end; - position['width'] = min(leftArea + width - 16, screenWidth * 0.618); + position.right = rightArea <= 0 ? 16.0 : rightArea; + position.crossAxisAlignment = CrossAxisAlignment.end; + position.width = min(leftArea + width - 16, screenWidth * 0.618); } else { - position['left'] = offset.dx <= 0 ? 16.0 : offset.dx; - position['width'] = min(rightArea + width - 16, screenWidth * 0.618); + position.left = offset.dx <= 0 ? 16.0 : offset.dx; + position.width = min(rightArea + width - 16, screenWidth * 0.618); } /// The distance on the right side is very large, it is more beautiful on the right side if (rightArea > 0.8 * topArea && rightArea > 0.8 * bottomArea) { - position['left'] = offset.dx + width + 16; - position['top'] = offset.dy - 4; - position['bottom'] = null; - position['right'] = null; - position['width'] = min(position['width'], rightArea * 0.8); + position.left = offset.dx + width + 16; + position.top = offset.dy - 4; + position.bottom = null; + position.right = null; + position.width = min(position.width, rightArea * 0.8); } /// The distance on the left is large, it is more beautiful on the left side if (leftArea > 0.8 * topArea && leftArea > 0.8 * bottomArea) { - position['right'] = rightArea + width + 16; - position['top'] = offset.dy - 4; - position['bottom'] = null; - position['left'] = null; - position['crossAxisAlignment'] = CrossAxisAlignment.end; - position['width'] = min(position['width'], leftArea * 0.8); + position.right = rightArea + width + 16; + position.top = offset.dy - 4; + position.bottom = null; + position.left = null; + position.crossAxisAlignment = CrossAxisAlignment.end; + position.width = min(position.width, leftArea * 0.8); } return position; } - - /// Use default theme. - /// - /// * [texts] is an array of text on the guide page. - /// * [buttonTextBuilder] is the method of generating button text. - /// * [maskClosable] has remove to intro class, deprecated and not working now, use like below. - /// {@tool snippet} - /// ```dart - /// final Intro intro = Intro( - /// maskClosable: true, - /// ); - /// ``` - /// {@end-tool} - /// the parameters are the current page index and the total number of pages in sequence. - static Widget Function(StepWidgetParams params) useDefaultTheme({ - required List texts, - required String Function(int currentStepIndex, int stepCount) - buttonTextBuilder, - @deprecated bool? maskClosable, - }) { - return (StepWidgetParams stepWidgetParams) { - int currentStepIndex = stepWidgetParams.currentStepIndex; - int stepCount = stepWidgetParams.stepCount; - Offset offset = stepWidgetParams.offset!; - - Map position = _smartGetPosition( - screenSize: stepWidgetParams.screenSize, - size: stepWidgetParams.size, - offset: offset, - ); - - return Stack( - children: [ - Positioned( - child: Container( - width: position['width'], - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: position['crossAxisAlignment'], - children: [ - Text( - currentStepIndex > texts.length - 1 - ? '' - : texts[currentStepIndex], - softWrap: true, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - height: 1.4, - color: Colors.white, - ), - ), - SizedBox( - height: 12, - ), - SizedBox( - height: 28, - child: OutlinedButton( - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all( - Colors.white, - ), - overlayColor: MaterialStateProperty.all( - Colors.white.withOpacity(0.1), - ), - side: MaterialStateProperty.all( - BorderSide( - color: Colors.white, - ), - ), - padding: MaterialStateProperty.all( - EdgeInsets.symmetric( - vertical: 0, - horizontal: 8, - ), - ), - shape: MaterialStateProperty.all( - StadiumBorder(), - ), - ), - onPressed: () { - if (stepCount - 1 == currentStepIndex) { - stepWidgetParams.onFinish(); - } else { - stepWidgetParams.onNext!(); - } - }, - child: Text( - buttonTextBuilder(currentStepIndex, stepCount), - style: TextStyle( - fontSize: 12, - ), - ), - ), - ), - ], - ), - ), - left: position['left'], - top: position['top'], - bottom: position['bottom'], - right: position['right'], - ), - ], - ); - }; - } - - /// Use advanced theme. - /// - /// * [widgetBuilder] the widget returned by this method will be displayed on the screen. - /// it's worth noting that the maximum display width of the widget will be limited by the current screen width. - static Widget Function(StepWidgetParams params) useAdvancedTheme({ - required Widget Function(StepWidgetParams params) widgetBuilder, - }) { - return (StepWidgetParams stepWidgetParams) { - Offset offset = stepWidgetParams.offset!; - - Map position = _smartGetPosition( - screenSize: stepWidgetParams.screenSize, - size: stepWidgetParams.size, - offset: offset, - ); - - return Stack( - children: [ - Positioned( - child: SizedBox( - width: position['width'], - child: widgetBuilder(stepWidgetParams), - ), - left: position['left'], - top: position['top'], - bottom: position['bottom'], - right: position['right'], - ), - ], - ); - }; - } } diff --git a/lib/step_widget_params.dart b/lib/step_widget_params.dart index 6ec9f4e..4d7aea2 100644 --- a/lib/step_widget_params.dart +++ b/lib/step_widget_params.dart @@ -12,12 +12,6 @@ class StepWidgetParams { /// End all guide page methods final VoidCallback onFinish; - /// Which guide page is currently displayed, starting from 0 - final int currentStepIndex; - - /// Total number of guide pages - final int stepCount; - /// The width and height of the screen final Size screenSize; @@ -27,19 +21,21 @@ class StepWidgetParams { /// The coordinates of the upper left corner of the highlighted component final Offset? offset; + /// The step order + final int order; + StepWidgetParams({ this.onPrev, this.onNext, + required this.order, required this.onFinish, required this.screenSize, required this.size, - required this.currentStepIndex, - required this.stepCount, required this.offset, }); @override String toString() { - return 'StepWidgetParams(currentStepIndex: $currentStepIndex, stepCount: $stepCount, size: $size, screenSize: $screenSize, offset: $offset)'; + return 'StepWidgetParams(order: $order, size: $size, screenSize: $screenSize, offset: $offset)'; } } diff --git a/pubspec.lock b/pubspec.lock index 1df6b69..26efb0f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,14 +42,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -73,7 +73,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -87,7 +87,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -134,21 +134,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml index c5b765a..3ef925c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_intro description: A better way for new feature introduction and step-by-step users guide for your Flutter project. -version: 2.3.1 -homepage: https://github.com/tal-tech/flutter_intro +version: 3.0.2 +homepage: https://github.com/minaxorg/flutter_intro environment: sdk: '>=2.12.0 <3.0.0' diff --git a/test/flutter_intro_test.dart b/test/flutter_intro_test.dart index 52e9806..ab73b3a 100644 --- a/test/flutter_intro_test.dart +++ b/test/flutter_intro_test.dart @@ -1,67 +1 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_intro/flutter_intro.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test( - 'intro info position test1', - () { - Map map = StepWidgetBuilder.smartGetPosition( - size: Size( - 10, - 10, - ), - screenSize: Size( - 100, - 100, - ), - offset: Offset( - 0, - 0, - ), - ); - expect( - map, - { - 'crossAxisAlignment': CrossAxisAlignment.start, - 'top': -4.0, - 'left': 26.0, - 'width': 61.8, - 'bottom': null, - 'right': null, - }, - ); - }, - ); - - test( - 'intro info position test2', - () { - Map map = StepWidgetBuilder.smartGetPosition( - size: Size( - 10, - 10, - ), - screenSize: Size( - 100, - 100, - ), - offset: Offset( - 90, - 90, - ), - ); - expect( - map, - { - 'crossAxisAlignment': CrossAxisAlignment.end, - 'top': 86.0, - 'left': null, - 'width': 61.8, - 'bottom': null, - 'right': 26.0, - }, - ); - }, - ); -} +void main() {}