Skip to content

Instantly share code, notes, and snippets.

@theachoem
Last active January 5, 2022 16:43
Show Gist options
  • Save theachoem/a5915a48411589a0808d93ea4deca7bd to your computer and use it in GitHub Desktop.
Save theachoem/a5915a48411589a0808d93ea4deca7bd to your computer and use it in GitHub Desktop.
ABA Mobile Android Hidden Drawer - Demo included (Flutter)
// 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();
}
}
}
@theachoem
Copy link
Author

theachoem commented Jun 6, 2021

DEMO:

120917440-8c5c6380-c6d9-11eb-9def-9a4ed37b3aa0-2.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment