Showing
14 changed files
with
731 additions
and
8 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 | ||
... | @@ -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 | ... | ... |
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