Created
August 30, 2021 10:24
-
-
Save orestesgaolin/cb23a8776b30e345d22e6e3fc2515a70 to your computer and use it in GitHub Desktop.
Basic Flutter implementation of the Google Photos draggable menu
This file contains 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
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('[email protected]'), | |
), | |
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