Skip to content

Instantly share code, notes, and snippets.

@orestesgaolin
Created August 30, 2021 10:24
Show Gist options
  • Save orestesgaolin/cb23a8776b30e345d22e6e3fc2515a70 to your computer and use it in GitHub Desktop.
Save orestesgaolin/cb23a8776b30e345d22e6e3fc2515a70 to your computer and use it in GitHub Desktop.
Basic Flutter implementation of the Google Photos draggable menu
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const ShowMenuWrapper(child: MyHomePage(title: 'Google Fothos')),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Scaffold(
appBar: AppBar(
title: Text(title),
actions: [
InkWell(
onTap: () {
ShowMenuWrapper.of(context).toggleMenu();
},
child: const _Avatar(),
),
],
),
body: const _Grid(),
),
if (ShowMenuWrapper.of(context).showMenu) const MenuPage(),
],
);
}
}
class MenuPage extends StatelessWidget {
const MenuPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.9,
builder: (context, scrollController) {
final canTakeInsets = scrollController.hasClients;
var insets = 16.0;
if (canTakeInsets) {
final height = MediaQuery.of(context).size.height;
insets =
((height - scrollController.position.viewportDimension) / 5 - 1)
.clamp(0.0, 16.0);
}
return Listener(
onPointerUp: (_) {
if (insets >= 16.0 && ShowMenuWrapper.of(context).showMenu) {
// ShowMenuWrapper.of(context).toggleMenu();
}
},
child: _ListContent(
insets: insets,
scrollController: scrollController,
),
);
},
);
}
}
class _ListContent extends StatelessWidget {
const _ListContent({
Key? key,
required this.insets,
required this.scrollController,
}) : super(key: key);
final double insets;
final ScrollController scrollController;
@override
Widget build(BuildContext context) {
final top = (32 - 2 * insets);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: insets,
),
child: Material(
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
ListView(
controller: scrollController,
physics: const ClampingScrollPhysics(),
padding: EdgeInsets.zero,
children: [
Padding(
padding: EdgeInsets.only(top: 16 + top, bottom: 8),
child: const FlutterLogo(
style: FlutterLogoStyle.horizontal,
),
),
const ListTile(
leading: _Avatar(),
title: Text('John Doe'),
subtitle: Text('example@example.com'),
),
ListTile(
title: OutlinedButton(
onPressed: () {},
child: const Text('Manage your account'),
),
),
const Divider(),
const ListTile(
leading: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.cloud_outlined),
),
title: Text('Account storage'),
subtitle: LinearProgressIndicator(
value: 0.5,
),
),
const Divider(),
const ListTile(
leading: Padding(
padding: EdgeInsets.all(8.0),
child: SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
title: Text('Save up to 100GB of data'),
subtitle: Text('Lorem ipsum dolor sit amet'),
),
const Divider(),
const ListTile(
leading: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.settings_outlined),
),
title: Text('Settings'),
),
],
),
Padding(
padding: EdgeInsets.only(top: top),
child: IconButton(
onPressed: () {
ShowMenuWrapper.of(context).toggleMenu();
},
icon: const Icon(Icons.close),
),
),
],
),
),
);
}
}
class ShowMenuWrapper extends StatefulWidget {
const ShowMenuWrapper({Key? key, required this.child}) : super(key: key);
final Widget child;
static _ShowMenuWrapperState of(BuildContext context) {
final _ShowMenuWrapperState? result =
context.dependOnInheritedWidgetOfExactType<ShowMenuController>()?.state;
assert(result != null, 'No ShowMenuController found in context');
return result!;
}
@override
_ShowMenuWrapperState createState() => _ShowMenuWrapperState();
}
class _ShowMenuWrapperState extends State<ShowMenuWrapper> {
bool showMenu = false;
void toggleMenu() {
setState(() {
showMenu = !showMenu;
});
}
@override
Widget build(BuildContext context) {
return ShowMenuController(
showMenu: showMenu,
child: widget.child,
state: this,
);
}
}
class ShowMenuController extends InheritedWidget {
const ShowMenuController({
Key? key,
required this.showMenu,
required Widget child,
required this.state,
}) : super(key: key, child: child);
final bool showMenu;
final _ShowMenuWrapperState state;
@override
bool updateShouldNotify(ShowMenuController oldWidget) =>
showMenu != oldWidget.showMenu;
}
class _Grid extends StatelessWidget {
const _Grid({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 3,
children: [
for (int i in List.generate(100, (index) => index))
ColoredBox(
// some random color
color: Color(
(0xFF000000 + i * (0xFFFFFFFF - 0xFF000000) / 100).toInt())),
],
);
}
}
class _Avatar extends StatelessWidget {
const _Avatar({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return const CircleAvatar(
backgroundImage: NetworkImage('https://thispersondoesnotexist.com/image'),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment