Last active
January 5, 2022 16:43
-
-
Save theachoem/a5915a48411589a0808d93ea4deca7bd to your computer and use it in GitHub Desktop.
ABA Mobile Android Hidden Drawer - Demo included (Flutter)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2021, Thea Choem, All rights reserved. | |
import 'dart:ui'; | |
import 'package:flutter/material.dart'; | |
typedef WrapperBuilder = Widget Function( | |
BuildContext context, | |
VoidCallback callback, | |
); | |
class ABADrawerWrapper extends StatefulWidget { | |
final WrapperBuilder drawerBuilder; | |
final WrapperBuilder childBuilder; | |
final double drawerWidth; | |
const ABADrawerWrapper({ | |
Key? key, | |
required this.childBuilder, | |
required this.drawerBuilder, | |
required this.drawerWidth, | |
}) : super(key: key); | |
@override | |
_ABADrawerWrapperState createState() => _ABADrawerWrapperState(); | |
} | |
class _ABADrawerWrapperState extends State<ABADrawerWrapper> with SingleTickerProviderStateMixin { | |
late Duration toggleDuration; | |
late double maxSlide; | |
late double minDragStartEdge; | |
late double maxDragStartEdge; | |
late AnimationController _animationController; | |
late ValueNotifier notifier; | |
bool _canBeDragged = false; | |
@override | |
void initState() { | |
super.initState(); | |
maxSlide = widget.drawerWidth; | |
minDragStartEdge = maxSlide; | |
maxDragStartEdge = maxSlide; | |
toggleDuration = const Duration(milliseconds: 300); | |
notifier = ValueNotifier<double>(0); | |
_animationController = AnimationController( | |
duration: toggleDuration, | |
vsync: this, | |
); | |
} | |
@override | |
void dispose() { | |
_animationController.dispose(); | |
notifier.dispose(); | |
super.dispose(); | |
} | |
void close() => _animationController.reverse(); | |
void open() => _animationController.forward(); | |
void toggleDrawer() => _animationController.isCompleted ? close() : open(); | |
@override | |
Widget build(BuildContext context) { | |
return WillPopScope( | |
onWillPop: () async { | |
if (_animationController.isCompleted) { | |
close(); | |
return false; | |
} | |
return true; | |
}, | |
child: Container( | |
color: Colors.white, | |
child: GestureDetector( | |
onHorizontalDragStart: _onDragStart, | |
onHorizontalDragUpdate: _onDragUpdate, | |
onHorizontalDragEnd: _onDragEnd, | |
child: Stack( | |
children: [ | |
buildChild(), | |
buildDrawer(), | |
], | |
), | |
), | |
), | |
); | |
} | |
Widget buildDrawer() { | |
return AnimatedBuilder( | |
animation: _animationController, | |
child: widget.drawerBuilder(context, () => open()), | |
builder: (context, child) { | |
CurveTween curve = CurveTween(curve: Curves.easeInOut); | |
double animValue = _animationController.drive(curve).value; | |
final slideAmount = maxSlide * animValue; | |
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { | |
notifier.value = slideAmount; | |
}); | |
return Container( | |
transform: Matrix4.identity()..translate(slideAmount - maxSlide), | |
alignment: Alignment.centerLeft, | |
child: child, | |
); | |
}, | |
); | |
} | |
Widget buildChild() { | |
return ValueListenableBuilder( | |
valueListenable: notifier, | |
child: buildChildOverlay(), | |
builder: (context, value, child) { | |
return AnimatedContainer( | |
curve: Curves.easeInOutSine, | |
duration: const Duration(milliseconds: 5), | |
transform: Matrix4.identity()..translate(notifier.value), | |
alignment: Alignment.centerLeft, | |
child: GestureDetector( | |
onTap: _animationController.isCompleted ? close : null, | |
child: child, | |
), | |
); | |
}, | |
); | |
} | |
Widget buildChildOverlay() { | |
return Stack( | |
fit: StackFit.expand, | |
children: [ | |
widget.childBuilder(context, () => toggleDrawer()), | |
AnimatedBuilder( | |
animation: _animationController, | |
builder: (context, child) { | |
double opacity = lerpDouble(0, 0.5, _animationController.value) ?? 0; | |
double shadowOpacity = lerpDouble(0.0, 0.25, _animationController.value) ?? 0; | |
return IgnorePointer( | |
ignoring: _animationController.isDismissed, | |
child: AnimatedContainer( | |
width: double.infinity, | |
duration: const Duration(milliseconds: 5), | |
decoration: BoxDecoration( | |
color: Colors.black.withOpacity(opacity), | |
boxShadow: [ | |
BoxShadow( | |
offset: Offset(4 - MediaQuery.of(context).size.width, 0.0), | |
blurRadius: 12.0, | |
color: Theme.of(context).shadowColor.withOpacity(shadowOpacity), | |
), | |
], | |
), | |
), | |
); | |
}, | |
), | |
], | |
); | |
} | |
void _onDragStart(DragStartDetails details) { | |
bool isDragOpenFromLeft = _animationController.isDismissed && details.globalPosition.dx < minDragStartEdge; | |
bool isDragCloseFromRight = _animationController.isCompleted && details.globalPosition.dx > maxDragStartEdge; | |
_canBeDragged = isDragOpenFromLeft || isDragCloseFromRight; | |
} | |
void _onDragUpdate(DragUpdateDetails details) { | |
if (!_canBeDragged) return; | |
double delta = (details.primaryDelta ?? 0) / maxSlide; | |
_animationController.value += delta; | |
} | |
void _onDragEnd(DragEndDetails details) { | |
if (_animationController.isDismissed || _animationController.isCompleted) { | |
return; | |
} | |
double _kMinFlingVelocity = 365.0; | |
double _width = MediaQuery.of(context).size.width; | |
if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) { | |
double visualVelocity = details.velocity.pixelsPerSecond.dx / _width; | |
_animationController.fling(velocity: visualVelocity); | |
} else if (_animationController.value < 0.5) { | |
close(); | |
} else { | |
open(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
DEMO:
120917440-8c5c6380-c6d9-11eb-9def-9a4ed37b3aa0-2.mov