diff --git a/example/lib/main_temp.dart b/example/lib/main_temp.dart new file mode 100644 index 00000000..62e2f4bf --- /dev/null +++ b/example/lib/main_temp.dart @@ -0,0 +1,202 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:getwidget/getwidget.dart'; + +final List imageList = [ + 'https://cdn.pixabay.com/photo/2017/12/03/18/04/christmas-balls-2995437_960_720.jpg', + 'https://cdn.pixabay.com/photo/2017/12/13/00/23/christmas-3015776_960_720.jpg', + 'https://cdn.pixabay.com/photo/2019/12/19/10/55/christmas-market-4705877_960_720.jpg', + 'https://cdn.pixabay.com/photo/2019/12/20/00/03/road-4707345_960_720.jpg', + 'https://cdn.pixabay.com/photo/2019/12/22/04/18/x-mas-4711785__340.jpg', + 'https://cdn.pixabay.com/photo/2016/11/22/07/09/spruce-1848543__340.jpg', + 'https://cdn.pixabay.com/photo/2017/12/03/18/04/christmas-balls-2995437_960_720.jpg', + 'https://cdn.pixabay.com/photo/2017/12/13/00/23/christmas-3015776_960_720.jpg', +]; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) => MaterialApp( + title: 'GetWidget Example', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + debugShowCheckedModeBanner: false, + home: const MyHomePage(title: 'GetWidget Example'), + ); +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State + with SingleTickerProviderStateMixin { + final _scaffoldKey = new GlobalKey(); + TabController tabController; + final _ratingController = TextEditingController(); + bool check = false; + String searchData; + final TextEditingController _searchController = TextEditingController(); + int groupValue = 0; + final GFBottomSheetController _controller = GFBottomSheetController(); + double foo = 100.0; + + @override + void initState() { + super.initState(); + _ratingController.text = '4.5'; + tabController = TabController(length: 6, initialIndex: 3, vsync: this); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + + bool switchValue = true; + bool showToast = false; + + List list = [ + 'Flutter', + 'React', + 'Ionic', + 'Xamarin', + 'Flutter2', + 'React2', + 'Ionic2', + 'Xamarin2', + ]; + + void _persistentBottomSheet() { + _scaffoldKey.currentState.showBottomSheet((context) => Container( + color: Colors.redAccent, + height: 250, + child: const Center( + child: Text('Hey! guys , this is a persistent bottom sheet'), + ), + )); + } + + void _modalBottomSheetMenu() { + showModalBottomSheet( + context: context, + elevation: 10, + builder: (builder) => Container( + height: 350, + color: Colors.transparent, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10))), + child: const Center( + child: Text('This is a modal sheet'), + )), + )); + } + + @override + Widget build(BuildContext context) => Scaffold( + key: _scaffoldKey, + appBar: GFAppBar( + backgroundColor: Colors.blueGrey, + title: const Text('UI Kit'), + centerTitle: true, + ), + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 10, + ), + Radio( + value: 0, + groupValue: groupValue, + onChanged: (val) { + setState(() { + groupValue = val; + }); + }, + ), + Radio( + value: 1, + groupValue: groupValue, + onChanged: (val) { + setState(() { + groupValue = val; + }); + }, + ), +// Expanded(child: SizedBox.expand( +// child: DraggableScrollableSheet( +// builder: (BuildContext context, ScrollController scrollController) => Container( +// color: Colors.blue[100], +// child: ListView.builder( +// controller: scrollController, +// itemCount: 25, +// itemBuilder: (BuildContext context, int index) => ListTile(title: Text('Item $index')), +// ), +// ), +// ), +// ),) + ], + ), + ), + bottomSheet: GFBottomSheet( + controller: _controller, +// minContentHeight: 100, + maxContentHeight: 500, +// elevation: 10, + stickyHeader: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.tealAccent), + height: 50, + child: const Center( + child: Text('Swipe me!'), + ), + ), + contentBody: Container( + child: ListView( + children: const [ + Text('fhj'), + Text('fhj'), + Text('fhj'), + Text('fhj'), + Text('fhj'), + Text('fhj'), + ], + ), + ), + stickyFooter: Container( + color: Theme.of(context).primaryColor, + height: 100, + child: const Center( + child: Text('I am Footer!'), + ), + ), + stickyFooterHeight: 50, + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.stars), + onPressed: () { +// _persistentBottomSheet(); +// _modalBottomSheetMenu(); + _controller.isBottomSheetOpened + ? _controller.hideBottomSheet() + : _controller.showBottomSheet(); + }), + ); +} diff --git a/lib/components/bottom_sheet/ex.dart b/lib/components/bottom_sheet/ex.dart new file mode 100644 index 00000000..aa63e0cf --- /dev/null +++ b/lib/components/bottom_sheet/ex.dart @@ -0,0 +1,413 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// [ExpandableBottomSheet] is a BottomSheet with a draggable height like the +/// Google Maps App on Android. +/// +/// __Example:__ +/// +/// ```dart +/// ExpandableBottomSheet( +/// background: Container( +/// color: Colors.red, +/// child: Center( +/// child: Text('Background'), +/// ), +/// ), +/// persistentHeader: Container( +/// height: 40, +/// color: Colors.blue, +/// child: Center( +/// child: Text('Header'), +/// ), +/// ), +/// expandableContent: Container( +/// height: 500, +/// color: Colors.green, +/// child: Center( +/// child: Text('Content'), +/// ), +/// ), +/// ); +/// ``` +class ExpandableBottomSheet extends StatefulWidget { + /// [expandableContent] is the widget which you can hide and show by dragging. + /// It has to be a widget with a constant height. It is required for the [ExpandableBottomSheet]. + final Widget expandableContent; + + /// [background] is the widget behind the [expandableContent] which holds + /// usually the content of your page. It is required for the [ExpandableBottomSheet]. + final Widget background; + + /// [persistentHeader] is a Widget which is on top of the [expandableContent] + /// and will never be hidden. It is made for a widget which indicates the + /// user he can expand the content by dragging. + final Widget persistentHeader; + + /// [persistentFooter] is a widget which is always shown at the bottom. The [expandableContent] + /// is if it is expanded on top of it so you don't need margin to see all of + /// your content. You can use it for example for navigation or a menu. + final Widget persistentFooter; + + /// [persistentContentHeight] is the height of the content which will never + /// been contracted. It only relates to [expandableContent]. [persistentHeader] + /// and [persistentFooter] will not be affected by this. + final double persistentContentHeight; + + /// [animationDurationExtend] is the duration for the animation if you stop + /// dragging with high speed. + final Duration animationDurationExtend; + + /// [animationDurationContract] is the duration for the animation to bottom + /// if you stop dragging with high speed. If it is `null` [animationDurationExtend] will be used. + final Duration animationDurationContract; + + /// [animationCurveExpand] is the curve of the animation for expanding + /// the [expandableContent] if the drag ended with high speed. + final Curve animationCurveExpand; + + /// [animationCurveContract] is the curve of the animation for contracting + /// the [expandableContent] if the drag ended with high speed. + final Curve animationCurveContract; + + /// [onIsExtendedCallback] will be executed if the extend reaches its maximum. + final Function() onIsExtendedCallback; + + /// [onIsContractedCallback] will be executed if the extend reaches its minimum. + final Function() onIsContractedCallback; + + /// Creates the [ExpandableBottomSheet]. + /// + /// [persistentContentHeight] has to be greater 0. + const ExpandableBottomSheet({ + Key key, + @required this.expandableContent, + @required this.background, + this.persistentHeader, + this.persistentFooter, + this.persistentContentHeight = 0.0, + this.animationCurveExpand = Curves.ease, + this.animationCurveContract = Curves.ease, + this.animationDurationExtend = const Duration(milliseconds: 250), + this.animationDurationContract = const Duration(milliseconds: 250), + this.onIsExtendedCallback, + this.onIsContractedCallback, + }) : assert(expandableContent != null), + assert(background != null), + assert(persistentContentHeight != null && persistentContentHeight >= 0), + assert(animationCurveExpand != null), + assert(animationCurveContract != null), + assert(animationDurationExtend != null), + assert(animationDurationContract != null), + super(key: key); + + @override + ExpandableBottomSheetState createState() => ExpandableBottomSheetState(); +} + +class ExpandableBottomSheetState extends State + with TickerProviderStateMixin { + GlobalKey _contentKey = new GlobalKey(debugLabel: 'contentKey'); + GlobalKey _headerKey = new GlobalKey(debugLabel: 'headerKey'); + GlobalKey _footerKey = new GlobalKey(debugLabel: 'footerKey'); + + AnimationController _controller; + + double _draggableHeight = 0; + double _positionOffset; + double _startOffsetAtDragDown = 0; + double _startPositionAtDragDown = 0; + + double _minOffset = 0; + double _maxOffset = 0; + + double _animationMinOffset = 0; + + AnimationStatus _oldStatus = AnimationStatus.dismissed; + + bool _useDrag = true; + bool _callCallbacks = false; + + /// Expands the content of the widget. + void expand() { + _afterUpdateWidgetBuild(false); + _callCallbacks = true; + _animateToTop(); + } + + /// Contracts the content of the widget. + void contract() { + _afterUpdateWidgetBuild(false); + _callCallbacks = true; + _animateToBottom(); + } + + /// The status of the expansion. + ExpansionStatus get expansionStatus { + if (_positionOffset == null) return ExpansionStatus.contracted; + if (_positionOffset == _maxOffset) return ExpansionStatus.contracted; + if (_positionOffset == _minOffset) return ExpansionStatus.expanded; + return ExpansionStatus.middle; + } + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + lowerBound: 0.0, + upperBound: 1.0, + ); + _controller.addStatusListener(_handleAnimationStatusUpdate); + WidgetsBinding.instance + .addPostFrameCallback((_) => _afterUpdateWidgetBuild(true)); + } + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance + .addPostFrameCallback((_) => _afterUpdateWidgetBuild(false)); + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Stack( + overflow: Overflow.clip, + children: [ + Align( + alignment: Alignment.topLeft, + child: widget.background, + ), + AnimatedBuilder( + animation: _controller, + builder: (_, Widget child) { + if (_controller.isAnimating) { + _positionOffset = _animationMinOffset + + _controller.value * _draggableHeight; + } + return Positioned( + top: _positionOffset, + right: 0.0, + left: 0.0, + child: child, + ); + }, + child: GestureDetector( + onVerticalDragDown: _dragDown, + onVerticalDragUpdate: _dragUpdate, + onVerticalDragEnd: _dragEnd, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + key: _headerKey, + child: widget.persistentHeader ?? Container()), + Container( + key: _contentKey, + child: widget.expandableContent, + ), + ], + ), + ), + ) + ], + ), + ), + Container( + key: _footerKey, child: widget.persistentFooter ?? Container()), + ], + ); + } + + void _handleAnimationStatusUpdate(AnimationStatus status) { + if (status == AnimationStatus.completed) { + if (_oldStatus == AnimationStatus.forward) { + setState(() { + _draggableHeight = _maxOffset - _minOffset; + _positionOffset = _minOffset; + }); + if (widget.onIsExtendedCallback != null && _callCallbacks) { + widget.onIsExtendedCallback(); + } + } + if (_oldStatus == AnimationStatus.reverse) { + setState(() { + _draggableHeight = _maxOffset - _minOffset; + _positionOffset = _maxOffset; + }); + if (widget.onIsContractedCallback != null && _callCallbacks) + widget.onIsContractedCallback(); + } + } + } + + void _afterUpdateWidgetBuild(bool isFirstBuild) { + double headerHeight = _headerKey.currentContext.size.height; + double footerHeight = _footerKey.currentContext.size.height; + double contentHeight = _contentKey.currentContext.size.height; + + double checkedPersistentContentHeight = + (widget.persistentContentHeight < contentHeight) + ? widget.persistentContentHeight + : contentHeight; + + _minOffset = + context.size.height - headerHeight - contentHeight - footerHeight; + _maxOffset = context.size.height - + headerHeight - + footerHeight - + checkedPersistentContentHeight; + + if (!isFirstBuild) { + _positionOutOfBounds(); + } else { + setState(() { + _positionOffset = _maxOffset; + _draggableHeight = _maxOffset - _minOffset; + }); + } + } + + void _positionOutOfBounds() { + if (_positionOffset < _minOffset) { + //the extend is larger than contentHeight + _callCallbacks = false; + _animateToMin(); + } else { + if (_positionOffset > _maxOffset) { + //the extend is smaller than persistentContentHeight + _callCallbacks = false; + _animateToMax(); + } else { + _draggableHeight = _maxOffset - _minOffset; + } + } + } + + void _animateOnIsAnimating() { + if (_controller.isAnimating) { + _controller.stop(); + } + } + + void _dragDown(DragDownDetails details) { + if (_controller.isAnimating) { + _useDrag = false; + } else { + _useDrag = true; + _startOffsetAtDragDown = details.localPosition.dy; + _startPositionAtDragDown = _positionOffset; + } + } + + void _dragUpdate(DragUpdateDetails details) { + if (!_useDrag) return; + double offset = details.localPosition.dy; + double newOffset = + _startPositionAtDragDown + offset - _startOffsetAtDragDown; + if (_minOffset <= newOffset && _maxOffset >= newOffset) { + setState(() { + _positionOffset = newOffset; + }); + } else { + if (_minOffset > newOffset) + setState(() { + _positionOffset = _minOffset; + }); + if (_maxOffset < newOffset) + setState(() { + _positionOffset = _maxOffset; + }); + } + } + + void _dragEnd(DragEndDetails details) { + if (_startPositionAtDragDown == _positionOffset || !_useDrag) return; + if (details.primaryVelocity < -250) { + //drag up ended with high speed + _callCallbacks = true; + _animateToTop(); + } else { + if (details.primaryVelocity > 250) { + //drag down ended with high speed + _callCallbacks = true; + _animateToBottom(); + } else { + if (_positionOffset == _maxOffset && + widget.onIsContractedCallback != null) { + widget.onIsContractedCallback(); + } + if (_positionOffset == _minOffset && + widget.onIsExtendedCallback != null) { + widget.onIsExtendedCallback(); + } + } + } + } + + void _animateToTop() { + _animateOnIsAnimating(); + _controller.value = (_positionOffset - _minOffset) / _draggableHeight; + _animationMinOffset = _minOffset; + _oldStatus = AnimationStatus.forward; + _controller.animateTo( + 0.0, + duration: widget.animationDurationExtend, + curve: widget.animationCurveExpand, + ); + } + + void _animateToBottom() { + _animateOnIsAnimating(); + + _controller.value = (_positionOffset - _minOffset) / _draggableHeight; + _animationMinOffset = _minOffset; + _oldStatus = AnimationStatus.reverse; + _controller.animateTo( + 1.0, + duration: + widget.animationDurationContract ?? widget.animationDurationExtend, + curve: widget.animationCurveContract ?? widget.animationCurveExpand, + ); + } + + void _animateToMax() { + _animateOnIsAnimating(); + + _controller.value = 1.0; + _draggableHeight = _positionOffset - _maxOffset; + _animationMinOffset = _maxOffset; + _oldStatus = AnimationStatus.reverse; + _controller.animateTo(0.0, + duration: widget.animationDurationExtend, + curve: widget.animationCurveExpand); + } + + void _animateToMin() { + _animateOnIsAnimating(); + + _controller.value = 1.0; + _draggableHeight = _positionOffset - _minOffset; + _animationMinOffset = _minOffset; + _oldStatus = AnimationStatus.forward; + _controller.animateTo( + 0.0, + duration: + widget.animationDurationContract ?? widget.animationDurationExtend, + curve: widget.animationCurveContract ?? widget.animationCurveExpand, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +/// The status of the expandable content. +enum ExpansionStatus { + expanded, + middle, + contracted, +} diff --git a/lib/components/bottom_sheet/gf_bottom_sheet.dart b/lib/components/bottom_sheet/gf_bottom_sheet.dart index 0834a9eb..8bb9df13 100644 --- a/lib/components/bottom_sheet/gf_bottom_sheet.dart +++ b/lib/components/bottom_sheet/gf_bottom_sheet.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:async'; class GFBottomSheet extends StatefulWidget { GFBottomSheet({ @@ -16,7 +17,6 @@ class GFBottomSheet extends StatefulWidget { super(key: key) { controller.height = minContentHeight; controller.smoothness = 500; -// controller == null ? controller = GFBottomSheetController() : Container(); } /// [minContentHeight] controls the minimum height of the content body. @@ -60,7 +60,7 @@ class _GFBottomSheetState extends State Function _controllerListener; void _onVerticalDragUpdate(data) { - _setNativeSmoothness(); + _setSmoothness(); if (((widget.controller.height - data.delta.dy) > widget.minContentHeight) && ((widget.controller.height - data.delta.dy) < @@ -71,8 +71,7 @@ class _GFBottomSheetState extends State } void _onVerticalDragEnd(data) { - _setUsersSmoothness(); - + _setSmoothness(); if (isDragDirectionUp && widget.controller.value) { _showBottomSheet(); } else if (!isDragDirectionUp && !widget.controller.value) { @@ -91,6 +90,7 @@ class _GFBottomSheetState extends State @override void initState() { super.initState(); + position = widget.minContentHeight; widget.controller.value = showBottomSheet; _controllerListener = () { widget.controller.value ? _showBottomSheet() : _hideBottomSheet(); @@ -98,56 +98,101 @@ class _GFBottomSheetState extends State widget.controller.addListener(_controllerListener); } + StreamController controller = StreamController.broadcast(); + double position; + + @override Widget build(BuildContext context) { + final BottomSheetThemeData bottomSheetTheme = + Theme.of(context).bottomSheetTheme; + final double elevation = + widget.elevation ?? bottomSheetTheme.elevation ?? 0; + final Widget bottomSheet = Column( - mainAxisSize: MainAxisSize.min, - children: [ - widget.stickyHeader == null - ? Container() - : GestureDetector( - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - onTap: _onTap, - child: widget.stickyHeader, - ), - AnimatedBuilder( - animation: widget.controller, - builder: (_, Widget child) => AnimatedContainer( - curve: Curves.easeOut, - duration: Duration(milliseconds: widget.controller.smoothness), - height: widget.controller.height, - child: GestureDetector( - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - onTap: _onTap, - child: widget.contentBody, - ), - ), + mainAxisSize: MainAxisSize.min, + children: [ + widget.stickyHeader == null + ? Container() + : GestureDetector( + onVerticalDragUpdate: _onVerticalDragUpdate, + onVerticalDragEnd: _onVerticalDragEnd, + onTap: _onTap, + child: widget.stickyHeader, + ), + + + StreamBuilder( + stream: controller.stream, + builder: (context, snapshot) => GestureDetector( + onVerticalDragUpdate: (DragUpdateDetails details){ + position = MediaQuery.of(context).size.height - details.globalPosition.dy; + print('fff'); + + if(position >= widget.maxContentHeight){ + print('max'); + position = widget.maxContentHeight; + }else if(position <= widget.minContentHeight){ + print('min'); + position = widget.minContentHeight; + _hideBottomSheet(); + } + controller.add(position); + + }, + onVerticalDragEnd: _onVerticalDragEnd, + onTap: _onTap, + behavior: HitTestBehavior.translucent, + child: Container( + color: Colors.red, + height: snapshot.hasData ? snapshot.data : widget.maxContentHeight*0.3, + width: double.infinity, + child: Text('jk'), + ) ), - widget.stickyFooter != null - ? AnimatedBuilder( - animation: widget.controller, - builder: (_, Widget child) => AnimatedContainer( - curve: Curves.easeOut, - duration: - Duration(milliseconds: widget.controller.smoothness), - height: widget.controller.height != widget.minContentHeight - ? widget.stickyFooterHeight - : 0.0, - child: GestureDetector( - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - onTap: _onTap, - child: widget.stickyFooter, - ), - ), - ) - : Container(), - ], - ); + ), + + +// AnimatedBuilder( +// animation: widget.controller, +// builder: (_, Widget child) => +// AnimatedContainer( +// curve: Curves.easeOut, +// duration: Duration(milliseconds: widget.controller.smoothness), +// height: widget.controller.height, +// child: GestureDetector( +// onVerticalDragUpdate: _onVerticalDragUpdate, +// onVerticalDragEnd: _onVerticalDragEnd, +// onTap: _onTap, +// child: widget.contentBody +// ), +// ), +// ), + + widget.stickyFooter != null + ? AnimatedBuilder( + animation: widget.controller, + builder: (_, Widget child) => AnimatedContainer( + curve: Curves.easeOut, + duration: Duration( + milliseconds: widget.controller.smoothness), + height: widget.controller.height != + widget.minContentHeight + ? widget.stickyFooterHeight + : 0.0, + child: GestureDetector( + onVerticalDragUpdate: _onVerticalDragUpdate, + onVerticalDragEnd: _onVerticalDragEnd, + onTap: _onTap, + child: widget.stickyFooter, + ), + ), + ) + : Container(), + ], + ); return Material( - elevation: widget.elevation, + elevation: elevation, child: bottomSheet, ); } @@ -166,11 +211,7 @@ class _GFBottomSheetState extends State super.dispose(); } - void _setUsersSmoothness() { - widget.controller.smoothness = 500; - } - - void _setNativeSmoothness() { + void _setSmoothness() { widget.controller.smoothness = 500; } }