Skip to content

Instantly share code, notes, and snippets.

@rydmike
Last active March 11, 2021 11:45
Show Gist options
  • Save rydmike/2a3efd05ba677fe98f65771c4e1fa62e to your computer and use it in GitHub Desktop.
Save rydmike/2a3efd05ba677fe98f65771c4e1fa62e to your computer and use it in GitHub Desktop.
Demo of Flutter WEB master regression with theme change and AnimatedCrossFade
// MIT License
// Copyright 2020 Mike Rydstrom
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
// ****************************************************************************
// Const and finals for setup and theme
const double kEdgePadding = 16.0;
const double kMaxContentWidth = 1200.0;
const double kExpandWidth = 250;
const double kShrinkWidth = 60;
const double kMinCardWidth = 200;
const int kMaxCards = 200;
// Custom Eden palette
// Color sources: https://www.w3schools.com/colors/colors_2019.asp
const Color lightPrimary = Color(0xFF264E36); // Eden 23%
const Color lightPrimaryVariant = Color(0xFF224430); // Eden 20%
const Color lightSecondary = Color(0xFF797b3a); // Guacamole 35%
const Color lightSecondaryVariant = Color(0xFF555729); // Guacamole 25%
final Color lightCanvas = Color.alphaBlend(
lightPrimary.withAlpha((256 * 0.07).toInt()), Colors.white);
const Color darkPrimary = Color(0xFF59a678); // Eden light 50%
const Color darkPrimaryVariant = Color(0xFF478560); // Eden light 40%
const Color darkSecondary = Color(0xFFd5d6a8); // Guacamole 75%
const Color darkSecondaryVariant = Color(0xFFbbbe74); // Guacamole 60%
final Color darkCanvas = Color.alphaBlend(
darkPrimary.withAlpha((256 * 0.12).toInt()), const Color(0xFF121212));
const ColorScheme lightScheme = ColorScheme.light(
primary: lightPrimary,
primaryVariant: lightPrimaryVariant,
secondary: lightSecondary,
secondaryVariant: lightSecondaryVariant,
);
const ColorScheme darkScheme = ColorScheme.dark(
primary: darkPrimary,
primaryVariant: darkPrimaryVariant,
secondary: darkSecondary,
secondaryVariant: darkSecondaryVariant,
);
final ThemeData lightTheme = ThemeData.from(colorScheme: lightScheme).copyWith(
canvasColor: lightCanvas,
toggleableActiveColor: lightScheme.secondary,
buttonTheme: const ButtonThemeData(
colorScheme: lightScheme,
textTheme: ButtonTextTheme.primary,
),
);
final ThemeData darkTheme = ThemeData.from(colorScheme: darkScheme).copyWith(
canvasColor: darkCanvas,
toggleableActiveColor: darkScheme.secondary,
buttonTheme: const ButtonThemeData(
colorScheme: darkScheme,
textTheme: ButtonTextTheme.primary,
),
);
// ****************************************************************************
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isDarkMode;
@override
void initState() {
isDarkMode = false;
super.initState();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Theme.of(context).brightness,
systemNavigationBarColor: theme.scaffoldBackgroundColor,
systemNavigationBarIconBrightness: theme.brightness == Brightness.light
? Brightness.dark
: Brightness.light,
),
);
return MaterialApp(
title: 'Issue Discovery',
debugShowCheckedModeBanner: false,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: DemoPage(
title: 'Issue Discovery',
setDarkMode: (bool value) {
setState(() {
isDarkMode = value;
});
},
),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({
Key key,
this.title,
this.setDarkMode,
}) : super(key: key);
final String title;
final ValueChanged<bool> setDarkMode;
@override
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
double width;
bool isSidePanelExpanded;
bool showSidePanel;
bool isDarkMode;
double maxWidth;
double cutWidth;
@override
void initState() {
width = kExpandWidth;
isSidePanelExpanded = true;
showSidePanel = true;
isDarkMode = false;
maxWidth = 300;
cutWidth = 200;
super.initState();
}
@override
Widget build(BuildContext context) {
final MediaQueryData media = MediaQuery.of(context);
final double topPadding = media.padding.top;
final double bottomPadding = media.padding.bottom;
final Size size = media.size;
final ThemeData theme = Theme.of(context);
final TextTheme textTheme = theme.textTheme;
final TextStyle headline6 = textTheme.headline6;
final TextStyle caption = theme.primaryTextTheme.caption;
return Row(
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kExpandWidth),
child: Material(
elevation: 0,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: width,
child: SideContent(
isVisible: showSidePanel,
menuWidth: kExpandWidth,
),
),
),
),
Expanded(
child: Scaffold(
extendBodyBehindAppBar: true,
extendBody: true,
appBar: AppBar(
title: Row(
children: <Widget>[
Text(widget.title),
// Show canvas size in the app bar
Expanded(
child: Text(
" W:${size.width.round()} H:${size.height.round()}",
style: caption,
textAlign: TextAlign.right,
),
),
],
),
centerTitle: false,
elevation: 0,
backgroundColor: Colors.transparent,
// Gradient partially transparent AppBar
flexibleSpace: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
gradient: LinearGradient(
begin: AlignmentDirectional.centerStart,
end: AlignmentDirectional.centerEnd,
colors: <Color>[
theme.primaryColor,
theme.primaryColor.withOpacity(0.7),
],
),
),
child: null,
),
),
body: Scrollbar(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kMaxContentWidth),
child: ListView(
padding: EdgeInsets.fromLTRB(
kEdgePadding,
topPadding + kToolbarHeight + kEdgePadding,
kEdgePadding,
kEdgePadding + bottomPadding,
),
children: <Widget>[
Text(
'AnimatedCrossFade issue on CanvasKit',
style: textTheme.headline4,
),
//
// Change theme
//
const Divider(),
Text('Issue', style: headline6),
const Text(
'Repeated theme switches modifes the behavior of '
'AnimatedCrossFade Widget on Master Web builds. '
'Toggle the theme multiple times '
'and observe how the mockup user profile above the '
'side menu changes behavior.\n\n'
''
'The mock user profile can be opened/closed by cliking on it. '
'It is using a simple AnimatedCrossFade Widget to do this.'),
SwitchListTile.adaptive(
title: const Text(
'Change theme mode',
),
value: isDarkMode,
onChanged: (bool value) {
setState(() {
isDarkMode = value;
widget.setDarkMode(isDarkMode);
});
},
),
const Divider(),
Text('Side panel operation', style: headline6),
const SelectableText(
'The side panel operation of this demo is a part of '
'issue https://github.com/flutter/flutter/issues/63740 ',
),
const Text(
'The problematic part in that issue has been isolated and removed '
'from this demo, but the side panel operation was kept available '
'in this demo as well.'),
SwitchListTile.adaptive(
title: const Text(
'Expanded and collapse the side panel',
),
subtitle: const Text(
'Turn OFF for a narrow side panel',
),
value: isSidePanelExpanded,
onChanged: (bool value) {
setState(() {
isSidePanelExpanded = value;
if (showSidePanel) {
if (isSidePanelExpanded) {
width = kExpandWidth;
} else {
width = kShrinkWidth;
}
} else {
width = 0;
}
});
},
),
SwitchListTile.adaptive(
title: const Text(
'Show and hide the side panel',
),
subtitle: const Text(
'Turn OFF to hide the side panel completely'),
value: showSidePanel,
onChanged: (bool value) {
setState(() {
showSidePanel = value;
if (showSidePanel) {
if (isSidePanelExpanded) {
width = kExpandWidth;
} else {
width = kShrinkWidth;
}
} else {
width = 0;
}
});
},
),
//
// Source code location info
//
const Divider(),
Text('Source code for this demo', style: headline6),
const SelectableText(
'https://gist.github.com/rydmike/2a3efd05ba677fe98f65771c4e1fa62e'),
const Divider(),
//
// A Card GridView with responsive column amount. This is
// just here to put some content on the page to stress it
// a bit during resizing and scaling.
//
const SizedBox(height: kEdgePadding),
Text('Colorful cards in a responsive GridView',
style: headline6),
LayoutBuilder(
builder:
(BuildContext context, BoxConstraints constraint) {
final int nrOfColumns =
(constraint.maxWidth.toInt()) ~/
kMinCardWidth.toInt();
return GridView.builder(
padding: const EdgeInsets.all(kEdgePadding),
shrinkWrap: true,
primary: false,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
nrOfColumns == 0 ? 1 : nrOfColumns,
mainAxisSpacing: kEdgePadding,
crossAxisSpacing: kEdgePadding,
childAspectRatio: 1.4,
),
itemCount: kMaxCards,
itemBuilder: (_, int index) => Card(
elevation: 3,
child: GridItem(
title: 'Card ${index + 1}',
color: Colors.primaries[
index % Colors.primaries.length][
Theme.of(context).brightness ==
Brightness.light
? 600
: 300],
),
),
);
},
)
],
),
),
),
),
),
),
],
);
}
}
// The side panel content
//
class SideContent extends StatefulWidget {
const SideContent({
Key key,
this.isVisible,
this.menuWidth,
}) : super(key: key);
final bool isVisible;
final double menuWidth;
@override
_SideContentState createState() => _SideContentState();
}
class _SideContentState extends State<SideContent> {
int selectedItem;
@override
void initState() {
selectedItem = 2;
super.initState();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints size) {
return OverflowBox(
alignment: AlignmentDirectional.topStart,
minWidth: 0.0,
maxWidth: widget.menuWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AppBar(
title: const Text('Side panel'),
leading: ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 56),
child: IconButton(
icon: const Icon(Icons.adjust),
onPressed: () {},
),
),
elevation: 0,
flexibleSpace: Container(
height: kToolbarHeight,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: null,
),
),
Expanded(
child: Container(
width: size.maxWidth,
decoration: BoxDecoration(
border: widget.isVisible
? BorderDirectional(
end: BorderSide(
color: Theme.of(context).dividerColor,
),
)
: null,
),
child: ClipRect(
child: OverflowBox(
alignment: AlignmentDirectional.topStart,
minWidth: 0.0,
maxWidth: widget.menuWidth,
child: ListView(
padding:
const EdgeInsets.only(), // Removes all edge insets
children: <Widget>[
const UserProfile(),
for (int i = 0; i < 8; i++)
SideItem(
width: size.maxWidth,
menuWidth: widget.menuWidth,
onTap: () {
setState(() {
selectedItem = i;
});
},
selected: selectedItem == i,
icon: Icons.account_circle,
label: "This is item $i",
showDivider: (i % 3 == 0) && i != 0,
),
],
),
),
),
),
),
],
),
);
},
);
}
}
// Menu side items, just to make the demo more real use case like
//
class SideItem extends StatelessWidget {
const SideItem({
Key key,
this.width,
this.menuWidth,
this.onTap,
this.selected = false,
this.icon,
this.label,
this.showDivider = false,
}) : super(key: key);
final double width;
final double menuWidth;
final VoidCallback onTap;
final bool selected;
final IconData icon;
final String label;
final bool showDivider;
@override
Widget build(BuildContext context) {
if (width < 5) {
return const SizedBox.shrink();
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (showDivider) const Divider(thickness: 1, height: 1),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 2, 5, 2),
child: Material(
clipBehavior: Clip.antiAlias,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(50 / 2.0),
bottomRight: Radius.circular(50 / 2.0),
),
color: selected
? Theme.of(context).colorScheme.primary.withAlpha(0x3d)
: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Container(
height: 50,
width: width - 5,
child: OverflowBox(
alignment: AlignmentDirectional.topStart,
minWidth: 0.0,
maxWidth: menuWidth,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints.tightFor(
width: 56, height: 56),
child: Icon(icon),
),
if (width < kShrinkWidth + 10)
const SizedBox.shrink()
else
Text(label)
],
),
),
),
),
),
),
],
);
}
}
}
// A demo user profile widget that we use as leading widget on the Flexfold
// menu and rail.
class UserProfile extends StatefulWidget {
const UserProfile();
@override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
bool collapsedProfile;
@override
void initState() {
collapsedProfile = true;
super.initState();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextTheme textTheme = theme.textTheme;
final TextTheme primaryTextTheme = theme.primaryTextTheme;
const double hPadding = 5.0;
final Widget closedProfile = ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: hPadding, vertical: 5),
onTap: () {
setState(() {
collapsedProfile = !collapsedProfile;
});
},
leading: CircleAvatar(
backgroundColor: theme.colorScheme.primary,
radius: kShrinkWidth / 2 - hPadding,
child: Text('MR',
style: primaryTextTheme.subtitle1.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.w600)),
),
title: Text('Mike Rydstrom',
style: textTheme.subtitle1.copyWith(fontWeight: FontWeight.w600)),
subtitle: const Text('Company Inc'),
trailing: const Icon(Icons.keyboard_arrow_down),
);
//
// An opened version of the mock user profile
final Widget openProfile = Column(
children: <Widget>[
// Inlcuded the closed profile above the opened button panel
closedProfile,
// Add some buttons for the opened state
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
const Spacer(),
OutlineButton(
onPressed: () {},
visualDensity: VisualDensity.comfortable,
padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
child: Column(
children: <Widget>[
const Icon(Icons.person),
Text('Profile', style: textTheme.overline),
],
),
),
const SizedBox(width: 10),
OutlineButton(
onPressed: () {},
visualDensity: VisualDensity.comfortable,
padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
child: Column(
children: <Widget>[
const Icon(Icons.people),
Text('Organization', style: textTheme.overline),
],
),
),
const SizedBox(width: 8),
],
),
// ),
),
],
);
return AnimatedCrossFade(
firstChild: closedProfile,
secondChild: openProfile,
crossFadeState: collapsedProfile
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
);
}
}
// The grid items for the gridview with cards, not relevant for this case,
// just in the demo app as filler to stress it a bit more during resizing.
class GridItem extends StatelessWidget {
const GridItem({Key key, this.title, this.color}) : super(key: key);
final String title;
final Color color;
@override
Widget build(BuildContext context) {
final Color textColor =
ThemeData.estimateBrightnessForColor(color) == Brightness.light
? Colors.black87
: Colors.white70;
return Container(
color: color,
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
title,
style: TextStyle(
color: textColor,
fontSize: 18,
),
),
Icon(Icons.apps, color: textColor),
],
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment