Showing
14 changed files
with
740 additions
and
17 deletions
| ... | @@ -51,7 +51,7 @@ android { | ... | @@ -51,7 +51,7 @@ android { |
| 51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | 51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |
| 52 | applicationId "pub.yiyan.parlando.Parlando" | 52 | applicationId "pub.yiyan.parlando.Parlando" |
| 53 | minSdkVersion 21 | 53 | minSdkVersion 21 |
| 54 | - targetSdkVersion 30 | 54 | + targetSdkVersion 31 |
| 55 | versionCode flutterVersionCode.toInteger() | 55 | versionCode flutterVersionCode.toInteger() |
| 56 | versionName flutterVersionName | 56 | versionName flutterVersionName |
| 57 | multiDexEnabled true | 57 | multiDexEnabled true |
| ... | @@ -78,5 +78,5 @@ flutter { | ... | @@ -78,5 +78,5 @@ flutter { |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | dependencies { | 80 | dependencies { |
| 81 | - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | 81 | + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" |
| 82 | } | 82 | } | ... | ... |
| ... | @@ -10,6 +10,7 @@ | ... | @@ -10,6 +10,7 @@ |
| 10 | <activity | 10 | <activity |
| 11 | android:name=".MainActivity" | 11 | android:name=".MainActivity" |
| 12 | android:launchMode="singleTop" | 12 | android:launchMode="singleTop" |
| 13 | + android:exported="true" | ||
| 13 | android:theme="@style/LaunchTheme" | 14 | android:theme="@style/LaunchTheme" |
| 14 | android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" | 15 | android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" |
| 15 | android:hardwareAccelerated="true" | 16 | android:hardwareAccelerated="true" | ... | ... |
lib/events/trans_event.dart
0 → 100644
| 1 | +import 'package:Parlando/events/trans_event.dart'; | ||
| 2 | +import 'package:Parlando/poem/poem_router.dart'; | ||
| 3 | +import 'package:Parlando/routers/fluro_navigator.dart'; | ||
| 4 | +import 'package:Parlando/widgets/radial/flutter_radial_menu.dart'; | ||
| 1 | import 'package:flutter/material.dart'; | 5 | import 'package:flutter/material.dart'; |
| 2 | import 'package:Parlando/account/page/account_page.dart'; | 6 | import 'package:Parlando/account/page/account_page.dart'; |
| 3 | import 'package:Parlando/poem/page/poem_page.dart'; | 7 | import 'package:Parlando/poem/page/poem_page.dart'; |
| ... | @@ -13,13 +17,51 @@ class Home extends StatefulWidget { | ... | @@ -13,13 +17,51 @@ class Home extends StatefulWidget { |
| 13 | _HomeState createState() => _HomeState(); | 17 | _HomeState createState() => _HomeState(); |
| 14 | } | 18 | } |
| 15 | 19 | ||
| 20 | +enum MenuOptions { | ||
| 21 | + audio, | ||
| 22 | + video, | ||
| 23 | +} | ||
| 24 | + | ||
| 16 | class _HomeState extends State<Home> with RestorationMixin { | 25 | class _HomeState extends State<Home> with RestorationMixin { |
| 17 | late List<Widget> _pageList; | 26 | late List<Widget> _pageList; |
| 18 | final PageController _pageController = PageController(); | 27 | final PageController _pageController = PageController(); |
| 19 | 28 | ||
| 20 | HomeProvider provider = HomeProvider(); | 29 | HomeProvider provider = HomeProvider(); |
| 30 | + final GlobalKey<RadialMenuState> _menuKey = GlobalKey<RadialMenuState>(); | ||
| 31 | + final List<RadialMenuItem<MenuOptions>> items = <RadialMenuItem<MenuOptions>>[ | ||
| 32 | + const RadialMenuItem<MenuOptions>( | ||
| 33 | + tooltip: 'audio', | ||
| 34 | + value: MenuOptions.audio, | ||
| 35 | + child: Icon( | ||
| 36 | + Icons.mic_none_outlined, | ||
| 37 | + ), | ||
| 38 | + iconColor: Colors.white, | ||
| 39 | + backgroundColor: Colors.blue, | ||
| 40 | + ), | ||
| 41 | + const RadialMenuItem<MenuOptions>( | ||
| 42 | + tooltip: "video", | ||
| 43 | + value: MenuOptions.video, | ||
| 44 | + child: Icon( | ||
| 45 | + Icons.video_call_outlined, | ||
| 46 | + ), | ||
| 47 | + iconColor: Colors.white, | ||
| 48 | + backgroundColor: Colors.green, | ||
| 49 | + ), | ||
| 50 | + ]; | ||
| 21 | 51 | ||
| 22 | - List<BottomNavigationBarItem>? _list; | 52 | + void _onItemSelected(MenuOptions value) { |
| 53 | + if (value == MenuOptions.video) { | ||
| 54 | + NavigatorUtils.push( | ||
| 55 | + context, | ||
| 56 | + '${PoemRouter.poemRecordAudioPage}?id=100', | ||
| 57 | + ); | ||
| 58 | + } else if (value == MenuOptions.audio) { | ||
| 59 | + NavigatorUtils.push( | ||
| 60 | + context, | ||
| 61 | + '${PoemRouter.poemRecordVideoPage}?data=100', | ||
| 62 | + ); | ||
| 63 | + } | ||
| 64 | + } | ||
| 23 | 65 | ||
| 24 | @override | 66 | @override |
| 25 | void initState() { | 67 | void initState() { |
| ... | @@ -47,21 +89,36 @@ class _HomeState extends State<Home> with RestorationMixin { | ... | @@ -47,21 +89,36 @@ class _HomeState extends State<Home> with RestorationMixin { |
| 47 | child: DoubleTapBackExitApp( | 89 | child: DoubleTapBackExitApp( |
| 48 | child: Scaffold( | 90 | child: Scaffold( |
| 49 | floatingActionButton: FloatingActionButton( | 91 | floatingActionButton: FloatingActionButton( |
| 50 | - onPressed: () {}, | 92 | + onPressed: () { |
| 93 | + eventBus.fire(TransEvent()); | ||
| 94 | + NavigatorUtils.push( | ||
| 95 | + context, | ||
| 96 | + '${PoemRouter.poemRecordVideoPage}?data=100', | ||
| 97 | + ); | ||
| 98 | + }, | ||
| 51 | tooltip: "发一言", | 99 | tooltip: "发一言", |
| 52 | backgroundColor: Colors.white, | 100 | backgroundColor: Colors.white, |
| 53 | child: const Icon( | 101 | child: const Icon( |
| 54 | - Icons.add, | 102 | + Icons.video_call_outlined, |
| 55 | color: Colors.black45, | 103 | color: Colors.black45, |
| 56 | ), | 104 | ), |
| 57 | ), | 105 | ), |
| 106 | + // floatingActionButton: SizedBox( | ||
| 107 | + // height: 60, | ||
| 108 | + // child: RadialMenu( | ||
| 109 | + // key: _menuKey, | ||
| 110 | + // items: items, | ||
| 111 | + // radius: 80.0, | ||
| 112 | + // onSelected: _onItemSelected, | ||
| 113 | + // progressAnimationDuration:const Duration(milliseconds: 1), | ||
| 114 | + // ), | ||
| 115 | + // ), | ||
| 58 | floatingActionButtonLocation: | 116 | floatingActionButtonLocation: |
| 59 | FloatingActionButtonLocation.centerDocked, | 117 | FloatingActionButtonLocation.centerDocked, |
| 60 | bottomNavigationBar: Consumer<HomeProvider>( | 118 | bottomNavigationBar: Consumer<HomeProvider>( |
| 61 | builder: (_, provider, __) { | 119 | builder: (_, provider, __) { |
| 62 | return BottomAppBar( | 120 | return BottomAppBar( |
| 63 | color: Colors.black45, | 121 | color: Colors.black45, |
| 64 | - shape: const CircularNotchedRectangle(), | ||
| 65 | child: Row( | 122 | child: Row( |
| 66 | mainAxisSize: MainAxisSize.max, | 123 | mainAxisSize: MainAxisSize.max, |
| 67 | mainAxisAlignment: MainAxisAlignment.spaceAround, | 124 | mainAxisAlignment: MainAxisAlignment.spaceAround, |
| ... | @@ -109,7 +166,6 @@ class _HomeState extends State<Home> with RestorationMixin { | ... | @@ -109,7 +166,6 @@ class _HomeState extends State<Home> with RestorationMixin { |
| 109 | ), | 166 | ), |
| 110 | ), | 167 | ), |
| 111 | ]), | 168 | ]), |
| 112 | - elevation: 5.0, | ||
| 113 | ); | 169 | ); |
| 114 | }, | 170 | }, |
| 115 | ), | 171 | ), | ... | ... |
| ... | @@ -8,7 +8,6 @@ import 'package:Parlando/poem/widgets/poem_user_comments.dart'; | ... | @@ -8,7 +8,6 @@ import 'package:Parlando/poem/widgets/poem_user_comments.dart'; |
| 8 | import 'package:Parlando/res/gaps.dart'; | 8 | import 'package:Parlando/res/gaps.dart'; |
| 9 | import 'package:Parlando/routers/fluro_navigator.dart'; | 9 | import 'package:Parlando/routers/fluro_navigator.dart'; |
| 10 | import 'package:Parlando/util/image_utils.dart'; | 10 | import 'package:Parlando/util/image_utils.dart'; |
| 11 | -import 'package:Parlando/widgets/bars/home_action_bar.dart'; | ||
| 12 | import 'package:Parlando/widgets/bars/home_menu_bar.dart'; | 11 | import 'package:Parlando/widgets/bars/home_menu_bar.dart'; |
| 13 | import 'package:Parlando/widgets/my_app_bar.dart'; | 12 | import 'package:Parlando/widgets/my_app_bar.dart'; |
| 14 | 13 | ... | ... |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:Parlando/events/trans_event.dart'; | ||
| 1 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 2 | import 'package:Parlando/category/category_router.dart'; | 5 | import 'package:Parlando/category/category_router.dart'; |
| 3 | import 'package:Parlando/poem/poem_router.dart'; | 6 | import 'package:Parlando/poem/poem_router.dart'; |
| ... | @@ -34,6 +37,7 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { | ... | @@ -34,6 +37,7 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { |
| 34 | final TikTokVideoListController _videoListController = | 37 | final TikTokVideoListController _videoListController = |
| 35 | TikTokVideoListController(); | 38 | TikTokVideoListController(); |
| 36 | List<UserVideo> videoDataList = []; | 39 | List<UserVideo> videoDataList = []; |
| 40 | + late StreamSubscription bus; | ||
| 37 | 41 | ||
| 38 | @override | 42 | @override |
| 39 | void didChangeAppLifecycleState(AppLifecycleState state) async { | 43 | void didChangeAppLifecycleState(AppLifecycleState state) async { |
| ... | @@ -46,6 +50,7 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { | ... | @@ -46,6 +50,7 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { |
| 46 | void dispose() { | 50 | void dispose() { |
| 47 | WidgetsBinding.instance!.removeObserver(this); | 51 | WidgetsBinding.instance!.removeObserver(this); |
| 48 | _videoListController.currentPlayer.pause(); | 52 | _videoListController.currentPlayer.pause(); |
| 53 | + bus.cancel(); | ||
| 49 | super.dispose(); | 54 | super.dispose(); |
| 50 | } | 55 | } |
| 51 | 56 | ||
| ... | @@ -58,19 +63,19 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { | ... | @@ -58,19 +63,19 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { |
| 58 | initialList: videoDataList | 63 | initialList: videoDataList |
| 59 | .map( | 64 | .map( |
| 60 | (e) => VPVideoController( | 65 | (e) => VPVideoController( |
| 61 | - videoInfo: e, | 66 | + videoInfo: e, |
| 62 | - builder: () => VideoPlayerController.asset(e.url), | 67 | + builder: () => VideoPlayerController.asset(e.url), |
| 63 | - ), | 68 | + ), |
| 64 | - ) | 69 | + ) |
| 65 | .toList(), | 70 | .toList(), |
| 66 | videoProvider: (int index, List<VPVideoController> list) async { | 71 | videoProvider: (int index, List<VPVideoController> list) async { |
| 67 | return videoDataList | 72 | return videoDataList |
| 68 | .map( | 73 | .map( |
| 69 | (e) => VPVideoController( | 74 | (e) => VPVideoController( |
| 70 | - videoInfo: e, | 75 | + videoInfo: e, |
| 71 | - builder: () => VideoPlayerController.asset(e.url), | 76 | + builder: () => VideoPlayerController.asset(e.url), |
| 72 | - ), | 77 | + ), |
| 73 | - ) | 78 | + ) |
| 74 | .toList(); | 79 | .toList(); |
| 75 | }, | 80 | }, |
| 76 | ); | 81 | ); |
| ... | @@ -87,6 +92,10 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { | ... | @@ -87,6 +92,10 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { |
| 87 | }, | 92 | }, |
| 88 | ); | 93 | ); |
| 89 | 94 | ||
| 95 | + bus = eventBus.on<TransEvent>().listen((event) { | ||
| 96 | + _videoListController.currentPlayer.pause(); | ||
| 97 | + }); | ||
| 98 | + | ||
| 90 | super.initState(); | 99 | super.initState(); |
| 91 | } | 100 | } |
| 92 | 101 | ||
| ... | @@ -167,7 +176,7 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { | ... | @@ -167,7 +176,7 @@ class _PoemPageState extends State<PoemPage> with WidgetsBindingObserver { |
| 167 | Widget poem = TikTokVidePoem( | 176 | Widget poem = TikTokVidePoem( |
| 168 | title: "每日一言", | 177 | title: "每日一言", |
| 169 | poem: | 178 | poem: |
| 170 | - "清晨入古寺,初日照高林。\n曲径通幽处,禅房花木深。\n山光悦鸟性,潭影空人心。\n万籁此都寂,但余钟磬音。\n", | 179 | + "清晨入古寺,初日照高林。\n曲径通幽处,禅房花木深。\n山光悦鸟性,潭影空人心。\n万籁此都寂,但余钟磬音。\n", |
| 171 | author: "----《题破山寺后禅院》常建", | 180 | author: "----《题破山寺后禅院》常建", |
| 172 | onShowDetail: () { | 181 | onShowDetail: () { |
| 173 | tkController.animateToPage(TikTokPagePosition.right); | 182 | tkController.animateToPage(TikTokPagePosition.right); | ... | ... |
lib/widgets/radial/flutter_radial_menu.dart
0 → 100644
| 1 | +import 'dart:math' as Math; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | + | ||
| 5 | +/// Draws an [ActionIcon] and [_ArcProgressPainter] that represent an active action. | ||
| 6 | +/// As the provided [Animation] progresses the ActionArc grows into a full | ||
| 7 | +/// circle and the ActionIcon moves along it. | ||
| 8 | +class ArcProgressIndicator extends StatelessWidget { | ||
| 9 | + // required | ||
| 10 | + final Animation<double> controller; | ||
| 11 | + final double radius; | ||
| 12 | + | ||
| 13 | + // optional | ||
| 14 | + final double startAngle; | ||
| 15 | + final double? width; | ||
| 16 | + | ||
| 17 | + /// The color to use when filling the arc. | ||
| 18 | + /// | ||
| 19 | + /// Defaults to the accent color of the current theme. | ||
| 20 | + final Color? color; | ||
| 21 | + final IconData icon; | ||
| 22 | + final Color? iconColor; | ||
| 23 | + final double? iconSize; | ||
| 24 | + | ||
| 25 | + // private | ||
| 26 | + final Animation<double> _progress; | ||
| 27 | + | ||
| 28 | + ArcProgressIndicator({ | ||
| 29 | + Key? key, | ||
| 30 | + required this.controller, | ||
| 31 | + required this.radius, | ||
| 32 | + this.startAngle = 0.0, | ||
| 33 | + this.width, | ||
| 34 | + this.color, | ||
| 35 | + required this.icon, | ||
| 36 | + this.iconColor, | ||
| 37 | + this.iconSize, | ||
| 38 | + }) : _progress = Tween(begin: 0.0, end: 1.0).animate(controller), | ||
| 39 | + super(key: key); | ||
| 40 | + | ||
| 41 | + @override | ||
| 42 | + Widget build(BuildContext context) { | ||
| 43 | + late TextPainter _iconPainter; | ||
| 44 | + final ThemeData theme = Theme.of(context); | ||
| 45 | + final Color? _iconColor = iconColor ?? theme.colorScheme.secondary; | ||
| 46 | + final double? _iconSize = iconSize ?? IconTheme.of(context).size; | ||
| 47 | + | ||
| 48 | + _iconPainter = TextPainter( | ||
| 49 | + textDirection: Directionality.of(context), | ||
| 50 | + text: TextSpan( | ||
| 51 | + text: String.fromCharCode(icon.codePoint), | ||
| 52 | + style: TextStyle( | ||
| 53 | + inherit: false, | ||
| 54 | + color: _iconColor, | ||
| 55 | + fontSize: _iconSize, | ||
| 56 | + fontFamily: icon.fontFamily, | ||
| 57 | + package: icon.fontPackage, | ||
| 58 | + ), | ||
| 59 | + ), | ||
| 60 | + )..layout(); | ||
| 61 | + | ||
| 62 | + return CustomPaint( | ||
| 63 | + painter: _ArcProgressPainter( | ||
| 64 | + controller: _progress, | ||
| 65 | + color: color ?? theme.colorScheme.secondary, | ||
| 66 | + radius: radius, | ||
| 67 | + width: width ?? _iconSize! * 2, | ||
| 68 | + startAngle: startAngle, | ||
| 69 | + icon: _iconPainter, | ||
| 70 | + ), | ||
| 71 | + ); | ||
| 72 | + } | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +class _ArcProgressPainter extends CustomPainter { | ||
| 76 | + // required | ||
| 77 | + final Animation<double> controller; | ||
| 78 | + final Color color; | ||
| 79 | + final double radius; | ||
| 80 | + final double width; | ||
| 81 | + | ||
| 82 | + // optional | ||
| 83 | + final double startAngle; | ||
| 84 | + final TextPainter icon; | ||
| 85 | + | ||
| 86 | + _ArcProgressPainter({ | ||
| 87 | + required this.controller, | ||
| 88 | + required this.color, | ||
| 89 | + required this.radius, | ||
| 90 | + required this.width, | ||
| 91 | + this.startAngle = 0.0, | ||
| 92 | + required this.icon, | ||
| 93 | + }) : super(repaint: controller); | ||
| 94 | + | ||
| 95 | + @override | ||
| 96 | + void paint(Canvas canvas, Size size) { | ||
| 97 | + Paint paint = Paint() | ||
| 98 | + ..color = color | ||
| 99 | + ..strokeWidth = width | ||
| 100 | + ..strokeCap = StrokeCap.round | ||
| 101 | + ..style = PaintingStyle.stroke; | ||
| 102 | + | ||
| 103 | + final double sweepAngle = controller.value * 2 * Math.pi; | ||
| 104 | + | ||
| 105 | + canvas.drawArc( | ||
| 106 | + Offset.zero & size, | ||
| 107 | + startAngle, | ||
| 108 | + sweepAngle, | ||
| 109 | + false, | ||
| 110 | + paint, | ||
| 111 | + ); | ||
| 112 | + | ||
| 113 | + double angle = startAngle + sweepAngle; | ||
| 114 | + Offset offset = Offset( | ||
| 115 | + (size.width / 2 - icon.size.width / 2) + radius * Math.cos(angle), | ||
| 116 | + (size.height / 2 - icon.size.height / 2) + radius * Math.sin(angle), | ||
| 117 | + ); | ||
| 118 | + | ||
| 119 | + icon.paint(canvas, offset); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + @override | ||
| 123 | + bool shouldRepaint(_ArcProgressPainter other) { | ||
| 124 | + return controller.value != other.controller.value || | ||
| 125 | + color != other.color || | ||
| 126 | + radius != other.radius || | ||
| 127 | + width != other.width || | ||
| 128 | + startAngle != other.startAngle || | ||
| 129 | + icon != other.icon; | ||
| 130 | + } | ||
| 131 | +} |
lib/widgets/radial/src/radial_menu.dart
0 → 100644
| 1 | +import 'dart:async'; | ||
| 2 | +import 'dart:math' as math; | ||
| 3 | + | ||
| 4 | +import 'package:flutter/material.dart'; | ||
| 5 | +import 'package:Parlando/widgets/radial/src/radial_menu_button.dart'; | ||
| 6 | +import 'package:Parlando/widgets/radial/src/radial_menu_center_button.dart'; | ||
| 7 | +import 'package:Parlando/widgets/radial/src/radial_menu_item.dart'; | ||
| 8 | + | ||
| 9 | +const double _radiansPerDegree = math.pi / 180; | ||
| 10 | +const double _startAngle = -120.0 * _radiansPerDegree; | ||
| 11 | + | ||
| 12 | +typedef ItemAngleCalculator = double Function(int index); | ||
| 13 | + | ||
| 14 | +/// A radial menu for selecting from a list of items. | ||
| 15 | +/// | ||
| 16 | +/// A radial menu lets the user select from a number of items. It displays a | ||
| 17 | +/// button that opens the menu, showing its items arranged in an arc. Selecting | ||
| 18 | +/// an item triggers the animation of a progress bar drawn at the specified | ||
| 19 | +/// [radius] around the central menu button. | ||
| 20 | +/// | ||
| 21 | +/// The type `T` is the type of the values the radial menu represents. All the | ||
| 22 | +/// entries in a given menu must represent values with consistent types. | ||
| 23 | +/// Typically, an enum is used. Each [RadialMenuItem] in [items] must be | ||
| 24 | +/// specialized with that same type argument. | ||
| 25 | +/// | ||
| 26 | +/// Requires one of its ancestors to be a [Material] widget. | ||
| 27 | +/// | ||
| 28 | +/// See also: | ||
| 29 | +/// | ||
| 30 | +/// * [RadialMenuItem], the widget used to represent the [items]. | ||
| 31 | +/// * [RadialMenuCenterButton], the button used to open and close the menu. | ||
| 32 | +class RadialMenu<T> extends StatefulWidget { | ||
| 33 | + /// Creates a dropdown button. | ||
| 34 | + /// | ||
| 35 | + /// The [items] must have distinct values. | ||
| 36 | + /// | ||
| 37 | + /// The [radius], [menuAnimationDuration], and [progressAnimationDuration] | ||
| 38 | + /// arguments must not be null (they all have defaults, so do not need to be | ||
| 39 | + /// specified). | ||
| 40 | + const RadialMenu({ | ||
| 41 | + Key? key, | ||
| 42 | + required this.items, | ||
| 43 | + required this.onSelected, | ||
| 44 | + this.radius = 100.0, | ||
| 45 | + this.menuAnimationDuration = const Duration(milliseconds: 1000), | ||
| 46 | + this.progressAnimationDuration = const Duration(milliseconds: 1000), | ||
| 47 | + }) : super(key: key); | ||
| 48 | + | ||
| 49 | + /// The list of possible items to select among. | ||
| 50 | + final List<RadialMenuItem<T>> items; | ||
| 51 | + | ||
| 52 | + /// Called when the user selects an item. | ||
| 53 | + final Function onSelected; // TODO why Function? not ValueChanged? | ||
| 54 | + | ||
| 55 | + /// The radius of the arc used to lay out the items and draw the progress bar. | ||
| 56 | + /// | ||
| 57 | + /// Defaults to 100.0. | ||
| 58 | + final double radius; | ||
| 59 | + | ||
| 60 | + /// Duration of the menu opening/closing animation. | ||
| 61 | + /// | ||
| 62 | + /// Defaults to 1000 milliseconds. | ||
| 63 | + final Duration menuAnimationDuration; | ||
| 64 | + | ||
| 65 | + /// Duration of the action activation progress arc animation. | ||
| 66 | + /// | ||
| 67 | + /// Defaults to 1000 milliseconds. | ||
| 68 | + final Duration progressAnimationDuration; | ||
| 69 | + | ||
| 70 | + @override | ||
| 71 | + RadialMenuState createState() => RadialMenuState(); | ||
| 72 | +} | ||
| 73 | + | ||
| 74 | +class RadialMenuState extends State<RadialMenu> with TickerProviderStateMixin { | ||
| 75 | + late AnimationController _menuAnimationController; | ||
| 76 | + late AnimationController _progressAnimationController; | ||
| 77 | + bool _isOpen = false; | ||
| 78 | + int _activeItemIndex = -1; | ||
| 79 | + | ||
| 80 | + // todo: xqwzts: allow users to pass in their own calculator as a param. | ||
| 81 | + // and change this to the default: radialItemAngleCalculator. | ||
| 82 | + double calculateItemAngle(int index) { | ||
| 83 | + double _itemSpacing = 120.0 / widget.items.length; | ||
| 84 | + return _startAngle + index * _itemSpacing * _radiansPerDegree; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @override | ||
| 88 | + void initState() { | ||
| 89 | + super.initState(); | ||
| 90 | + _menuAnimationController = AnimationController( | ||
| 91 | + duration: widget.menuAnimationDuration, | ||
| 92 | + vsync: this, | ||
| 93 | + ); | ||
| 94 | + _progressAnimationController = AnimationController( | ||
| 95 | + duration: widget.progressAnimationDuration, | ||
| 96 | + vsync: this, | ||
| 97 | + ); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + @override | ||
| 101 | + void dispose() { | ||
| 102 | + _menuAnimationController.dispose(); | ||
| 103 | + _progressAnimationController.dispose(); | ||
| 104 | + super.dispose(); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + void _openMenu() { | ||
| 108 | + _menuAnimationController.forward(); | ||
| 109 | + setState(() => _isOpen = true); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + void _closeMenu() { | ||
| 113 | + _menuAnimationController.reverse(); | ||
| 114 | + setState(() => _isOpen = false); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + Future<void> _activate(int itemIndex) async { | ||
| 118 | + setState(() => _activeItemIndex = itemIndex); | ||
| 119 | + await _progressAnimationController.forward().orCancel; | ||
| 120 | + widget.onSelected(widget.items[itemIndex].value); | ||
| 121 | + _closeMenu(); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + /// Resets the menu to its initial (closed) state. | ||
| 125 | + void reset() { | ||
| 126 | + _menuAnimationController.reset(); | ||
| 127 | + _progressAnimationController.reverse(); | ||
| 128 | + setState(() { | ||
| 129 | + _isOpen = false; | ||
| 130 | + _activeItemIndex = -1; | ||
| 131 | + }); | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + Widget _buildActionButton(int index) { | ||
| 135 | + final RadialMenuItem item = widget.items[index]; | ||
| 136 | + | ||
| 137 | + return LayoutId( | ||
| 138 | + id: '${_RadialMenuLayout.actionButton}$index', | ||
| 139 | + child: RadialMenuButton( | ||
| 140 | + child: item, | ||
| 141 | + backgroundColor: item.backgroundColor, | ||
| 142 | + onPressed: () => _activate(index), | ||
| 143 | + ), | ||
| 144 | + ); | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + Widget _buildCenterButton() { | ||
| 148 | + return LayoutId( | ||
| 149 | + id: _RadialMenuLayout.menuButton, | ||
| 150 | + child: RadialMenuCenterButton( | ||
| 151 | + openCloseAnimationController: _menuAnimationController.view, | ||
| 152 | + activateAnimationController: _progressAnimationController.view, | ||
| 153 | + isOpen: _isOpen, | ||
| 154 | + onPressed: _isOpen ? _closeMenu : _openMenu, | ||
| 155 | + ), | ||
| 156 | + ); | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + @override | ||
| 160 | + Widget build(BuildContext context) { | ||
| 161 | + final List<Widget> children = <Widget>[]; | ||
| 162 | + for (int i = 0; i < widget.items.length; i++) { | ||
| 163 | + if (_activeItemIndex != i) { | ||
| 164 | + children.add(_buildActionButton(i)); | ||
| 165 | + } | ||
| 166 | + } | ||
| 167 | + children.add(_buildCenterButton()); | ||
| 168 | + | ||
| 169 | + return AnimatedBuilder( | ||
| 170 | + animation: _menuAnimationController, | ||
| 171 | + builder: (BuildContext context, Widget? child) { | ||
| 172 | + return CustomMultiChildLayout( | ||
| 173 | + delegate: _RadialMenuLayout( | ||
| 174 | + itemCount: widget.items.length, | ||
| 175 | + radius: widget.radius, | ||
| 176 | + calculateItemAngle: calculateItemAngle, | ||
| 177 | + controller: _menuAnimationController.view, | ||
| 178 | + ), | ||
| 179 | + children: children, | ||
| 180 | + ); | ||
| 181 | + }, | ||
| 182 | + ); | ||
| 183 | + } | ||
| 184 | +} | ||
| 185 | + | ||
| 186 | +class _RadialMenuLayout extends MultiChildLayoutDelegate { | ||
| 187 | + static const String menuButton = 'menuButton'; | ||
| 188 | + static const String actionButton = 'actionButton'; | ||
| 189 | + static const String activeAction = 'activeAction'; | ||
| 190 | + | ||
| 191 | + final int itemCount; | ||
| 192 | + final double radius; | ||
| 193 | + final ItemAngleCalculator calculateItemAngle; | ||
| 194 | + | ||
| 195 | + final Animation<double> controller; | ||
| 196 | + | ||
| 197 | + final Animation<double> _progress; | ||
| 198 | + | ||
| 199 | + _RadialMenuLayout({ | ||
| 200 | + required this.itemCount, | ||
| 201 | + required this.radius, | ||
| 202 | + required this.calculateItemAngle, | ||
| 203 | + required this.controller, | ||
| 204 | + }) : _progress = Tween<double>(begin: 0.0, end: radius).animate( | ||
| 205 | + CurvedAnimation( | ||
| 206 | + curve: Curves.elasticOut, | ||
| 207 | + parent: controller, | ||
| 208 | + ), | ||
| 209 | + ); | ||
| 210 | + | ||
| 211 | + late Offset center; | ||
| 212 | + | ||
| 213 | + @override | ||
| 214 | + void performLayout(Size size) { | ||
| 215 | + center = Offset(size.width / 2, size.height / 2); | ||
| 216 | + | ||
| 217 | + if (hasChild(menuButton)) { | ||
| 218 | + Size menuButtonSize; | ||
| 219 | + menuButtonSize = layoutChild(menuButton, BoxConstraints.loose(size)); | ||
| 220 | + | ||
| 221 | + // place the menubutton in the center | ||
| 222 | + positionChild( | ||
| 223 | + menuButton, | ||
| 224 | + Offset( | ||
| 225 | + center.dx - menuButtonSize.width / 2, | ||
| 226 | + center.dy - menuButtonSize.height / 2, | ||
| 227 | + ), | ||
| 228 | + ); | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + for (int i = 0; i < itemCount; i++) { | ||
| 232 | + final String actionButtonId = '$actionButton$i'; | ||
| 233 | + final String actionArcId = '$activeAction$i'; | ||
| 234 | + if (hasChild(actionArcId)) { | ||
| 235 | + final Size arcSize = layoutChild( | ||
| 236 | + actionArcId, | ||
| 237 | + BoxConstraints.expand( | ||
| 238 | + width: _progress.value * 2, | ||
| 239 | + height: _progress.value * 2, | ||
| 240 | + ), | ||
| 241 | + ); | ||
| 242 | + | ||
| 243 | + positionChild( | ||
| 244 | + actionArcId, | ||
| 245 | + Offset( | ||
| 246 | + center.dx - arcSize.width / 2, | ||
| 247 | + center.dy - arcSize.height / 2, | ||
| 248 | + ), | ||
| 249 | + ); | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + if (hasChild(actionButtonId)) { | ||
| 253 | + final Size buttonSize = | ||
| 254 | + layoutChild(actionButtonId, BoxConstraints.loose(size)); | ||
| 255 | + | ||
| 256 | + final double itemAngle = calculateItemAngle(i); | ||
| 257 | + | ||
| 258 | + positionChild( | ||
| 259 | + actionButtonId, | ||
| 260 | + Offset( | ||
| 261 | + (center.dx - buttonSize.width / 2) + | ||
| 262 | + (_progress.value) * math.cos(itemAngle), | ||
| 263 | + (center.dy - buttonSize.height / 2) + | ||
| 264 | + (_progress.value) * math.sin(itemAngle), | ||
| 265 | + ), | ||
| 266 | + ); | ||
| 267 | + } | ||
| 268 | + } | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + @override | ||
| 272 | + bool shouldRelayout(_RadialMenuLayout oldDelegate) => | ||
| 273 | + itemCount != oldDelegate.itemCount || | ||
| 274 | + radius != oldDelegate.radius || | ||
| 275 | + calculateItemAngle != oldDelegate.calculateItemAngle || | ||
| 276 | + controller != oldDelegate.controller || | ||
| 277 | + _progress != oldDelegate._progress; | ||
| 278 | +} |
| 1 | +import 'package:flutter/foundation.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | + | ||
| 4 | +class RadialMenuButton extends StatelessWidget { | ||
| 5 | + const RadialMenuButton({ | ||
| 6 | + Key? key, | ||
| 7 | + required this.child, | ||
| 8 | + required this.backgroundColor, | ||
| 9 | + required this.onPressed, | ||
| 10 | + }) : super(key: key); | ||
| 11 | + | ||
| 12 | + final Widget child; | ||
| 13 | + final Color backgroundColor; | ||
| 14 | + final VoidCallback onPressed; | ||
| 15 | + | ||
| 16 | + @override | ||
| 17 | + Widget build(BuildContext context) { | ||
| 18 | + final Color color = backgroundColor; | ||
| 19 | + | ||
| 20 | + return Semantics( | ||
| 21 | + button: true, | ||
| 22 | + enabled: true, | ||
| 23 | + child: Material( | ||
| 24 | + type: MaterialType.circle, | ||
| 25 | + color: color, | ||
| 26 | + child: InkWell( | ||
| 27 | + onTap: onPressed, | ||
| 28 | + child: child, | ||
| 29 | + ), | ||
| 30 | + ), | ||
| 31 | + ); | ||
| 32 | + } | ||
| 33 | +} |
| 1 | +import 'package:flutter/foundation.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | +import 'package:Parlando/widgets/radial/src/radial_menu_button.dart'; | ||
| 4 | + | ||
| 5 | +const double _defaultButtonSize = 48.0; | ||
| 6 | + | ||
| 7 | +/// The button at the center of a [RadialMenu] which controls its open/closed | ||
| 8 | +/// state. | ||
| 9 | +class RadialMenuCenterButton extends StatelessWidget { | ||
| 10 | + /// Drives the opening/closing animation of the [RadialMenu]. | ||
| 11 | + final Animation<double> openCloseAnimationController; | ||
| 12 | + | ||
| 13 | + /// Drives the animation when an item in the [RadialMenu] is pressed. | ||
| 14 | + final Animation<double> activateAnimationController; | ||
| 15 | + | ||
| 16 | + /// Called when the user presses this button. | ||
| 17 | + final VoidCallback onPressed; | ||
| 18 | + | ||
| 19 | + /// The opened/closed state of the menu. | ||
| 20 | + /// | ||
| 21 | + /// Determines which of [closedColor] or [openedColor] should be used as the | ||
| 22 | + /// background color of the button. | ||
| 23 | + final bool isOpen; | ||
| 24 | + | ||
| 25 | + /// The color to use when painting the icon. | ||
| 26 | + /// | ||
| 27 | + /// Defaults to [Colors.black]. | ||
| 28 | + final Color iconColor; | ||
| 29 | + | ||
| 30 | + /// Background color when it is in its closed state. | ||
| 31 | + /// | ||
| 32 | + /// Defaults to [Colors.white]. | ||
| 33 | + final Color closedColor; | ||
| 34 | + | ||
| 35 | + /// Background color when it is in its opened state. | ||
| 36 | + /// | ||
| 37 | + /// Defaults to [Colors.grey]. | ||
| 38 | + final Color openedColor; | ||
| 39 | + | ||
| 40 | + /// The size of the button. | ||
| 41 | + /// | ||
| 42 | + /// Defaults to 48.0. | ||
| 43 | + final double size; | ||
| 44 | + | ||
| 45 | + /// The animation progress for the [AnimatedIcon] in the center of the button. | ||
| 46 | + final Animation<double> _progress; | ||
| 47 | + | ||
| 48 | + /// The scale factor applied to the button. | ||
| 49 | + /// | ||
| 50 | + /// Animates from 1.0 to 0.0 when an an item is pressed in the menu and | ||
| 51 | + /// [activateAnimationController] progresses. | ||
| 52 | + final Animation<double> _scale; | ||
| 53 | + | ||
| 54 | + RadialMenuCenterButton({ | ||
| 55 | + Key? key, | ||
| 56 | + required this.openCloseAnimationController, | ||
| 57 | + required this.activateAnimationController, | ||
| 58 | + required this.onPressed, | ||
| 59 | + required this.isOpen, | ||
| 60 | + this.iconColor = Colors.black, | ||
| 61 | + this.closedColor = Colors.white, | ||
| 62 | + this.openedColor = Colors.grey, | ||
| 63 | + this.size = _defaultButtonSize, | ||
| 64 | + }) : _progress = Tween(begin: 0.0, end: 1.0).animate( | ||
| 65 | + CurvedAnimation( | ||
| 66 | + parent: openCloseAnimationController, | ||
| 67 | + curve: const Interval( | ||
| 68 | + 0.0, | ||
| 69 | + 0.5, | ||
| 70 | + curve: Curves.ease, | ||
| 71 | + ), | ||
| 72 | + ), | ||
| 73 | + ), | ||
| 74 | + _scale = Tween(begin: 1.0, end: 0.0).animate( | ||
| 75 | + CurvedAnimation( | ||
| 76 | + parent: activateAnimationController, | ||
| 77 | + curve: Curves.elasticIn, | ||
| 78 | + ), | ||
| 79 | + ), | ||
| 80 | + super(key: key); | ||
| 81 | + | ||
| 82 | + @override | ||
| 83 | + Widget build(BuildContext context) { | ||
| 84 | + final AnimatedIcon animatedIcon = AnimatedIcon( | ||
| 85 | + color: iconColor, | ||
| 86 | + icon: AnimatedIcons.menu_close, | ||
| 87 | + progress: _progress, | ||
| 88 | + ); | ||
| 89 | + | ||
| 90 | + final Widget child = SizedBox( | ||
| 91 | + width: size, | ||
| 92 | + height: size, | ||
| 93 | + child: Center( | ||
| 94 | + child: animatedIcon, | ||
| 95 | + ), | ||
| 96 | + ); | ||
| 97 | + | ||
| 98 | + final Color color = isOpen ? openedColor : closedColor; | ||
| 99 | + | ||
| 100 | + return ScaleTransition( | ||
| 101 | + scale: _scale, | ||
| 102 | + child: RadialMenuButton( | ||
| 103 | + child: child, | ||
| 104 | + backgroundColor: color, | ||
| 105 | + onPressed: onPressed, | ||
| 106 | + ), | ||
| 107 | + ); | ||
| 108 | + } | ||
| 109 | +} |
lib/widgets/radial/src/radial_menu_item.dart
0 → 100644
| 1 | +import 'package:flutter/foundation.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | + | ||
| 4 | +const double _defaultButtonSize = 48.0; | ||
| 5 | + | ||
| 6 | +/// An item in a [RadialMenu]. | ||
| 7 | +/// | ||
| 8 | +/// The type `T` is the type of the value the entry represents. All the entries | ||
| 9 | +/// in a given menu must represent values with consistent types. | ||
| 10 | +class RadialMenuItem<T> extends StatelessWidget { | ||
| 11 | + /// Creates a circular action button for an item in a [RadialMenu]. | ||
| 12 | + /// | ||
| 13 | + /// The [child] argument is required. | ||
| 14 | + const RadialMenuItem({ | ||
| 15 | + Key? key, | ||
| 16 | + required this.child, | ||
| 17 | + required this.value, | ||
| 18 | + required this.tooltip, | ||
| 19 | + this.size = _defaultButtonSize, | ||
| 20 | + required this.backgroundColor, | ||
| 21 | + required this.iconColor, | ||
| 22 | + this.iconSize = 24.0, | ||
| 23 | + }) : super(key: key); | ||
| 24 | + | ||
| 25 | + /// The widget below this widget in the tree. | ||
| 26 | + /// | ||
| 27 | + /// Typically an [Icon] widget. | ||
| 28 | + final Widget child; | ||
| 29 | + | ||
| 30 | + /// The value to return if the user selects this menu item. | ||
| 31 | + /// | ||
| 32 | + /// Eventually returned in a call to [RadialMenu.onSelected]. | ||
| 33 | + final T value; | ||
| 34 | + | ||
| 35 | + /// Text that describes the action that will occur when the button is pressed. | ||
| 36 | + /// | ||
| 37 | + /// This text is displayed when the user long-presses on the button and is | ||
| 38 | + /// used for accessibility. | ||
| 39 | + final String tooltip; | ||
| 40 | + | ||
| 41 | + /// The color to use when filling the button. | ||
| 42 | + /// | ||
| 43 | + /// Defaults to the primary color of the current theme. | ||
| 44 | + final Color backgroundColor; | ||
| 45 | + | ||
| 46 | + /// The size of the button. | ||
| 47 | + /// | ||
| 48 | + /// Defaults to 48.0. | ||
| 49 | + final double size; | ||
| 50 | + | ||
| 51 | + /// The color to use when painting the child icon. | ||
| 52 | + /// | ||
| 53 | + /// Defaults to the primary icon theme color. | ||
| 54 | + final Color? iconColor; | ||
| 55 | + | ||
| 56 | + final double? iconSize; | ||
| 57 | + | ||
| 58 | + @override | ||
| 59 | + Widget build(BuildContext context) { | ||
| 60 | + final Color? _iconColor = | ||
| 61 | + iconColor ?? Theme.of(context).primaryIconTheme.color; | ||
| 62 | + | ||
| 63 | + late Widget result; | ||
| 64 | + | ||
| 65 | + result = Center( | ||
| 66 | + child: IconTheme.merge( | ||
| 67 | + data: IconThemeData( | ||
| 68 | + color: _iconColor, | ||
| 69 | + size: iconSize, | ||
| 70 | + ), | ||
| 71 | + child: child, | ||
| 72 | + ), | ||
| 73 | + ); | ||
| 74 | + | ||
| 75 | + result = Tooltip( | ||
| 76 | + message: tooltip, | ||
| 77 | + child: result, | ||
| 78 | + ); | ||
| 79 | + | ||
| 80 | + result = SizedBox( | ||
| 81 | + width: size, | ||
| 82 | + height: size, | ||
| 83 | + child: result, | ||
| 84 | + ); | ||
| 85 | + | ||
| 86 | + return result; | ||
| 87 | + } | ||
| 88 | +} |
| ... | @@ -309,6 +309,13 @@ packages: | ... | @@ -309,6 +309,13 @@ packages: |
| 309 | url: "https://pub.flutter-io.cn" | 309 | url: "https://pub.flutter-io.cn" |
| 310 | source: hosted | 310 | source: hosted |
| 311 | version: "2.0.1" | 311 | version: "2.0.1" |
| 312 | + event_bus: | ||
| 313 | + dependency: "direct main" | ||
| 314 | + description: | ||
| 315 | + name: event_bus | ||
| 316 | + url: "https://pub.flutter-io.cn" | ||
| 317 | + source: hosted | ||
| 318 | + version: "2.0.0" | ||
| 312 | fake_async: | 319 | fake_async: |
| 313 | dependency: transitive | 320 | dependency: transitive |
| 314 | description: | 321 | description: | ... | ... |
| ... | @@ -111,6 +111,7 @@ dependencies: | ... | @@ -111,6 +111,7 @@ dependencies: |
| 111 | 111 | ||
| 112 | getwidget: ^2.0.5 | 112 | getwidget: ^2.0.5 |
| 113 | sign_in_with_apple: ^3.3.0 | 113 | sign_in_with_apple: ^3.3.0 |
| 114 | + event_bus: ^2.0.0 | ||
| 114 | 115 | ||
| 115 | dependency_overrides: | 116 | dependency_overrides: |
| 116 | decimal: 1.5.0 | 117 | decimal: 1.5.0 | ... | ... |
-
Please register or login to post a comment