diff --git a/CHANGELOG.md b/CHANGELOG.md index 4314768..5b2e576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [1.1.0] - July 2021 + +* Add Controller and update example. + ## [1.0.2] - July 2021 * Add images. diff --git a/README.md b/README.md index 8e6c2a5..5fb8d66 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,36 @@ A Flutter side bar slider can be with any widget you want * Customizable `clicker`, you can change the clicker widget with what you want. * Just like Flutter's `Drawer`, but with new way. +## Things to note + +* The `SlidableBar` should have specific width and height, it works fine with scaffold's body. +* The `SlidableBar` doesn't create it's own context, so it will appear under floating action button if you add one. + ## Usage ```dart + final SlidableBarController controller = SlidableBarController(initialStatus: true); + SlidableBar( - width: 50, // this will be the height when you change the side to bottom or top - frontColor: Colors.green, // the color of the cycle inside the default clicker + size: 50, // this will be bar's height in case Side is bottom or top and width in case Side is left or right children: [], // here is the widgets inside the bar - child: Container(), // here is your page body + child: Container(), // here is your widget that will be under the bar // optional + frontColor: Colors.green, // the color of the cycle inside the default clicker + slidableController: controller, // the contoller to change and listen to the bar status + barRadius: const BorderRadius.circular(10.0), // the contoller to change and listen to the bar status side: Side.bottom, // Side.right is the default clickerPosition: 1.0, // the position of the clicker, 0.0 is the default clickerSize: 60, // the sizer of the default clicker, 55 is the default onChange: (int index){ - print(index); - // this will print the index of the widget you clicked inside the bar + print(index); // this will print the index of the widget you clicked inside the bar }, duration: Duration(milliseconds: 500), // the duration of the animation, (300 mil) is the default isOpenFirst: true, // the initial state of the bar, false is default backgroundColor: Colors.black, // primary color is default curve: Curves.ease, // the animation curve, linear is defualt clicker: Icon(Icons.arrow_forward_ios), // this will build instead of the default clicker - ); + ) ``` ## Getting started @@ -40,7 +48,7 @@ In the `pubspec.yaml` of your flutter project, add the following dependency: ```yaml dependencies: - slidable_bar: "^1.0.0" + slidable_bar: "^1.1.0" ``` Then run `$ flutter pub get`. In your library, add the following import: diff --git a/example/lib/main.dart b/example/lib/main.dart index fac61de..bc89c09 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -13,26 +13,51 @@ class App extends StatelessWidget { } } -class Example extends StatelessWidget { +class Example extends StatefulWidget { + + @override + _ExampleState createState() => _ExampleState(); +} + +class _ExampleState extends State { + + final SlidableBarController controller = SlidableBarController(initialStatus: true); + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('slidable bar example'), ), - body: SlidableBar( - width: 60, - frontColor: Colors.green, - backgroundColor: Colors.white, - barChildren: [ - FlutterLogo(size: 50,), - FlutterLogo(size: 50,), - FlutterLogo(size: 50,), - FlutterLogo(size: 50,), + body: Column( + children: [ + Container( + height: 210, + width: 300, + child: SlidableBar( + size: 60, + slidableController: controller, + side: Side.top, + barChildren: [ + FlutterLogo(size: 50,), + FlutterLogo(size: 50,), + FlutterLogo(size: 50,), + FlutterLogo(size: 50,), + ], + child: Container( + color: Colors.grey.shade200, + child: Center( + child: ElevatedButton( + child: Text("reverse status"), + onPressed: (){ + controller.reverseStatus(); + }, + ), + ), + ), + ), + ), ], - child: Container( - color: Colors.grey.shade200, - ), ), ); } diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 747db1d..a5eb090 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -1,30 +1,4 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); } diff --git a/lib/slidable_bar.dart b/lib/slidable_bar.dart index a34a91f..091dbe5 100644 --- a/lib/slidable_bar.dart +++ b/lib/slidable_bar.dart @@ -1,278 +1,5 @@ library slidable_bar; -import 'dart:async'; -import 'package:flutter/material.dart'; - -class SlidableBar extends StatefulWidget { - - /// This child is the widget that will present the side bar above it - final Widget child; - - /// This is for align the bar and it can be - /// [Side.right] the default, [Side.left], [Side.top] and [Side.bottom] - final Side side; - - /// This for animation - final Duration duration; - - /// This to set the initial state [true, false] - final bool isOpenFirst; - - /// This will call when ever the user click on any item inside the bae - final ValueChanged? onChange; - - /// The children inside the bar - final List barChildren; - - /// This is backgroundColor for the default clicker and bar - final Color? backgroundColor; - - /// This is the color of the cycle inside the default clicker - final Color frontColor; - - /// This is the curve of the animation - final Curve curve; - - /// When the side is bottom or top: - /// [width] will be the height of the bar - /// else it will be the width - final double width; - - /// If you want do add your own clicker - final Widget? clicker; - - /// This for align the clicker - final double clickerPosition; - - /// This is the size of the default clicker - final double clickerSize; - - const SlidableBar( - {Key? key, - required this.child, - required this.barChildren, - required this.width, - required this.frontColor, - this.clicker, - this.onChange, - this.side = Side.right, - this.duration = const Duration(milliseconds: 300), - this.backgroundColor, - this.clickerPosition = 0.0, - this.clickerSize = 55, - this.curve = Curves.linear, - this.isOpenFirst = false}) - : super(key: key); - - @override - _SlidableSideBarState createState() => _SlidableSideBarState(); -} - -class _SlidableSideBarState extends State { - StreamController barStatus = StreamController(); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: barStatus.stream, - initialData: widget.isOpenFirst, - builder: (context, snapshot) { - final isOpened = snapshot.data!; - return Stack( - fit: StackFit.expand, - children: [ - Positioned.fill(child: widget.child), - if (widget.clicker == null) - AnimatedPositioned( - right: [Side.bottom, Side.top].contains(widget.side) - ? 0 - : widget.side == Side.right - ? isOpened - ? widget.width - (widget.clickerSize * 0.54) - : -(widget.clickerSize * 0.54) - : null, - left: [Side.bottom, Side.top].contains(widget.side) - ? 0 - : widget.side == Side.left - ? isOpened - ? widget.width - (widget.clickerSize * 0.54) - : -(widget.clickerSize * 0.54) - : null, - bottom: [Side.right, Side.left].contains(widget.side) - ? 0 - : widget.side == Side.bottom - ? isOpened - ? widget.width - (widget.clickerSize * 0.54) - : -(widget.clickerSize * 0.54) - : null, - top: [Side.right, Side.left].contains(widget.side) - ? 0 - : widget.side == Side.top - ? isOpened - ? widget.width - (widget.clickerSize * 0.54) - : -(widget.clickerSize * 0.54) - : null, - duration: widget.duration, - curve: widget.curve, - child: Align( - alignment: Alignment(widget.clickerPosition, widget.clickerPosition), - child: RotationTransition( - turns: AlwaysStoppedAnimation((widget.side == Side.right - ? 315 - : widget.side == Side.left - ? 135 - : widget.side == Side.bottom - ? 45 - : 225) / - 360), - child: InkWell( - onTap: () { - barStatus.add(!isOpened); - }, - child: Container( - width: widget.clickerSize, - height: widget.clickerSize, - decoration: BoxDecoration( - color: widget.backgroundColor ?? Theme.of(context).primaryColor, - borderRadius: BorderRadius.only(topLeft: Radius.circular(7)), - boxShadow: [ - BoxShadow(color: Colors.black12, spreadRadius: 1, blurRadius: 5), - ]), - alignment: Alignment.topLeft, - child: Container( - width: widget.clickerSize * 0.23, - height: widget.clickerSize * 0.23, - margin: EdgeInsets.all(6), - decoration: BoxDecoration(color: widget.frontColor, shape: BoxShape.circle), - ), - ), - ), - ), - ), - ), - AnimatedPositioned( - right: widget.side == Side.right - ? isOpened - ? 0 - : -widget.width - : widget.side == Side.left - ? null - : 0, - top: widget.side == Side.top - ? isOpened - ? 0 - : -widget.width - : widget.side == Side.bottom - ? null - : 0, - bottom: widget.side == Side.bottom - ? isOpened - ? 0 - : -widget.width - : widget.side == Side.top - ? null - : 0, - left: widget.side == Side.left - ? isOpened - ? 0 - : -widget.width - : widget.side == Side.right - ? null - : 0, - duration: widget.duration, - curve: widget.curve, - child: _SideBarContent( - children: widget.barChildren, - isOpen: isOpened, - width: widget.width, - onChange: widget.onChange, - controller: barStatus, - clicker: widget.clicker, - backgroundColor: widget.backgroundColor, - clickerPosition: widget.clickerPosition, - side: widget.side)), - ], - ); - }); - } - - @override - void dispose() { - barStatus.close(); - super.dispose(); - } -} - -class _SideBarContent extends StatelessWidget { - final List children; - final Color? backgroundColor; - final bool isOpen; - final double width; - final Widget? clicker; - final ValueChanged? onChange; - final StreamController controller; - final double clickerPosition; - final Side side; - - const _SideBarContent({ - Key? key, - required this.children, - required this.isOpen, - required this.width, - required this.controller, - required this.backgroundColor, - required this.onChange, - required this.clicker, - required this.clickerPosition, - required this.side, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final body = Container( - width: [Side.left, Side.right].contains(side) ? width : null, - height: [Side.left, Side.right].contains(side) ? null : width, - decoration: BoxDecoration( - boxShadow: [ - BoxShadow(color: Colors.black12, spreadRadius: 0, blurRadius: 5), - ], - color: backgroundColor ?? Theme.of(context).primaryColor, - ), - child: ListView.builder( - itemCount: children.length, - scrollDirection: [Side.right, Side.left].contains(side) ? Axis.vertical : Axis.horizontal, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - controller.add(false); - onChange?.call(index); - }, - child: children[index], - ); - }, - ), - ); - final customClicker = Align( - alignment: Alignment(clickerPosition, clickerPosition), - child: GestureDetector(onTap: () => controller.add(!isOpen), child: clicker)); - if ([Side.left, Side.right].contains(side)) { - return Row( - children: [ - if (clicker != null && side == Side.left) customClicker, - body, - if (clicker != null && side == Side.right) customClicker - ], - ); - } else { - return Column( - children: [ - if (clicker != null && side == Side.bottom) customClicker, - body, - if (clicker != null && side == Side.top) customClicker - ], - ); - } - } -} - -enum Side { top, bottom, right, left } +export 'src/slidable_bar_widget.dart'; +export 'src/side.dart'; +export 'src/slidable_bar_controller.dart'; \ No newline at end of file diff --git a/lib/src/side.dart b/lib/src/side.dart new file mode 100644 index 0000000..81bcd51 --- /dev/null +++ b/lib/src/side.dart @@ -0,0 +1,2 @@ + +enum Side { top, bottom, right, left } \ No newline at end of file diff --git a/lib/src/slidable_bar_controller.dart b/lib/src/slidable_bar_controller.dart new file mode 100644 index 0000000..091a620 --- /dev/null +++ b/lib/src/slidable_bar_controller.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +class SlidableBarController { + final bool initialStatus; + + SlidableBarController({this.initialStatus = false}) : currentStatus = initialStatus; + + bool currentStatus; + StreamController _barStatus = StreamController.broadcast(); + + Stream get statusStream => _barStatus.stream; + + hide() { + currentStatus = false; + _barStatus.add(currentStatus); + } + + show() { + currentStatus = true; + _barStatus.add(currentStatus); + } + + reverseStatus() { + currentStatus = !currentStatus; + _barStatus.add(currentStatus); + } + + dispose() { + _barStatus.close(); + } +} \ No newline at end of file diff --git a/lib/src/slidable_bar_widget.dart b/lib/src/slidable_bar_widget.dart new file mode 100644 index 0000000..c440bf0 --- /dev/null +++ b/lib/src/slidable_bar_widget.dart @@ -0,0 +1,259 @@ + +import 'package:flutter/material.dart'; +import 'side.dart'; +import 'slidable_bar_controller.dart'; + +class SlidableBar extends StatefulWidget { + final Widget child; + final Side side; + final Duration duration; + final ValueChanged? onChange; + final List barChildren; + final Color? backgroundColor; + final Color? frontColor; + final Curve curve; + final double size; + final Widget? clicker; + final BorderRadius? barRadius; + final double clickerPosition; + final double clickerSize; + final SlidableBarController? slidableController; + + const SlidableBar( + {Key? key, + required this.child, + required this.barChildren, + required this.size, + this.frontColor, + this.clicker, + this.onChange, + this.side = Side.right, + this.duration = const Duration(milliseconds: 300), + this.backgroundColor, + this.clickerPosition = 0.0, + this.clickerSize = 55, + this.curve = Curves.linear, + this.slidableController, + this.barRadius + }) : super(key: key); + + @override + _SlidableSideBarState createState() => _SlidableSideBarState(); +} + +class _SlidableSideBarState extends State { + late SlidableBarController controller; + + @override + void initState() { + super.initState(); + controller = widget.slidableController ?? SlidableBarController(); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: controller.statusStream, + initialData: controller.initialStatus, + builder: (context, snapshot) { + final isOpened = snapshot.data!; + return Stack( + fit: StackFit.expand, + children: [ + Positioned.fill(child: widget.child), + if (widget.clicker == null) + AnimatedPositioned( + right: [Side.bottom, Side.top].contains(widget.side) + ? 0 + : widget.side == Side.right + ? isOpened + ? widget.size - (widget.clickerSize * 0.54) + : -(widget.clickerSize * 0.54) + : null, + left: [Side.bottom, Side.top].contains(widget.side) + ? 0 + : widget.side == Side.left + ? isOpened + ? widget.size - (widget.clickerSize * 0.54) + : -(widget.clickerSize * 0.54) + : null, + bottom: [Side.right, Side.left].contains(widget.side) + ? 0 + : widget.side == Side.bottom + ? isOpened + ? widget.size - (widget.clickerSize * 0.54) + : -(widget.clickerSize * 0.54) + : null, + top: [Side.right, Side.left].contains(widget.side) + ? 0 + : widget.side == Side.top + ? isOpened + ? widget.size - (widget.clickerSize * 0.54) + : -(widget.clickerSize * 0.54) + : null, + duration: widget.duration, + curve: widget.curve, + child: Align( + alignment: Alignment(widget.clickerPosition, widget.clickerPosition), + child: RotationTransition( + turns: AlwaysStoppedAnimation((widget.side == Side.right + ? 315 + : widget.side == Side.left + ? 135 + : widget.side == Side.bottom + ? 45 + : 225) / + 360), + child: InkWell( + onTap: ()=> controller.reverseStatus(), + child: Container( + width: widget.clickerSize, + height: widget.clickerSize, + decoration: BoxDecoration( + color: widget.backgroundColor ?? Theme.of(context).colorScheme.onBackground, + borderRadius: BorderRadius.only(topLeft: Radius.circular(7)), + boxShadow: [ + BoxShadow(color: Colors.black12, spreadRadius: 1, blurRadius: 5), + ]), + alignment: Alignment.topLeft, + child: Container( + width: widget.clickerSize * 0.23, + height: widget.clickerSize * 0.23, + margin: EdgeInsets.all(6), + decoration: BoxDecoration(color: widget.frontColor ?? Theme.of(context).primaryColor, shape: BoxShape.circle), + ), + ), + ), + ), + ), + ), + AnimatedPositioned( + right: widget.side == Side.right + ? isOpened + ? 0 + : -widget.size + : widget.side == Side.left + ? null + : 0, + top: widget.side == Side.top + ? isOpened + ? 0 + : -widget.size + : widget.side == Side.bottom + ? null + : 0, + bottom: widget.side == Side.bottom + ? isOpened + ? 0 + : -widget.size + : widget.side == Side.top + ? null + : 0, + left: widget.side == Side.left + ? isOpened + ? 0 + : -widget.size + : widget.side == Side.right + ? null + : 0, + duration: widget.duration, + curve: widget.curve, + child: _SideBarContent( + children: widget.barChildren, + isOpen: isOpened, + width: widget.size, + onChange: widget.onChange, + controller: controller, + clicker: widget.clicker, + backgroundColor: widget.backgroundColor, + clickerPosition: widget.clickerPosition, + side: widget.side, + barRadius: widget.barRadius + )), + ], + ); + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } +} + +class _SideBarContent extends StatelessWidget { + final List children; + final Color? backgroundColor; + final bool isOpen; + final double width; + final Widget? clicker; + final ValueChanged? onChange; + final SlidableBarController controller; + final double clickerPosition; + final Side side; + final BorderRadius? barRadius; + + const _SideBarContent({ + Key? key, + required this.children, + required this.isOpen, + required this.width, + required this.controller, + required this.backgroundColor, + required this.onChange, + required this.clicker, + required this.clickerPosition, + required this.side, + required this.barRadius, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final body = Container( + width: [Side.left, Side.right].contains(side) ? width : null, + height: [Side.left, Side.right].contains(side) ? null : width, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow(color: Colors.black12, spreadRadius: 0, blurRadius: 5), + ], + color: backgroundColor ?? Theme.of(context).colorScheme.onBackground, + borderRadius: barRadius, + ), + clipBehavior: Clip.antiAlias, + child: ListView.builder( + itemCount: children.length, + scrollDirection: [Side.right, Side.left].contains(side) ? Axis.vertical : Axis.horizontal, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + controller.hide(); + onChange?.call(index); + }, + child: children[index], + ); + }, + ), + ); + final customClicker = Align( + alignment: Alignment(clickerPosition, clickerPosition), + child: GestureDetector(onTap: ()=> controller.reverseStatus(), child: clicker)); + if ([Side.left, Side.right].contains(side)) { + return Row( + children: [ + if (clicker != null && side == Side.left) customClicker, + body, + if (clicker != null && side == Side.right) customClicker + ], + ); + } else { + return Column( + children: [ + if (clicker != null && side == Side.bottom) customClicker, + body, + if (clicker != null && side == Side.top) customClicker + ], + ); + } + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 752faf2..3eeb408 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: slidable_bar description: A Flutter package to show SideBar and hide it and custimaze your clicker. -version: 1.0.2 +version: 1.1.0 issue_tracker: https://github.com/mahmoud-haj-ali/slidable_bar/issues homepage: https://github.com/mahmoud-haj-ali/slidable_bar