Skip to content

Instantly share code, notes, and snippets.

@rydmike
Last active June 18, 2020 15:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rydmike/114c36d75084ab57716faea36993b8ce to your computer and use it in GitHub Desktop.
Save rydmike/114c36d75084ab57716faea36993b8ce to your computer and use it in GitHub Desktop.
Flutter Material clipping issue with shadows
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(IssueDemoApp());
}
class IssueDemoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.red,
scaffoldBackgroundColor: Colors.white,
buttonTheme: ButtonThemeData(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.red),
textTheme: ButtonTextTheme.primary,
),
),
debugShowCheckedModeBanner: false,
home: ClipRectIssueDemo(),
);
}
}
class ClipRectIssueDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ClipRectIssueDemoState();
}
}
class _ClipRectIssueDemoState extends State<ClipRectIssueDemo> {
double _size = 200.0;
double _tightFactor = 1.0;
final double _borderRadius = 20;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Clipping Issue Demo v2'),
centerTitle: true,
elevation: 0,
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 20),
Text('Clipped Material with Shadows',
style: Theme.of(context).textTheme.headline6),
const SizedBox(height: 20),
GradientGlowButton(
size: _size,
elevation: 10,
borderRadius: _borderRadius,
),
const SizedBox(height: 30),
Center(
child: SizedBox(
width: 450,
child: ListTile(
title: const Text('Change demo widget size'),
subtitle: Slider(
min: 150.0,
max: 400.0,
divisions: (400 - 100).floor(),
label: _size.floor().toString(),
value: _size,
onChanged: (value) {
setState(() {
_size = value;
});
},
),
trailing: Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
const Text(
'Size',
style: TextStyle(fontSize: 11),
),
Text(
_size.floor().toString(),
style: const TextStyle(fontSize: 15),
),
],
),
),
),
),
),
Text('Tight Clipped Material',
style: Theme.of(context).textTheme.headline6),
const SizedBox(height: 20),
GradientGlowightClipperButton(
size: _size,
elevation: 10,
borderRadius: _borderRadius,
tightFactor: _tightFactor,
),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 450,
child: ListTile(
title: const Text('Device pixel ratio is '),
trailing: Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
MediaQuery.of(context)
.devicePixelRatio
.toStringAsFixed(2),
style: const TextStyle(fontSize: 15),
),
),
),
),
),
Center(
child: SizedBox(
width: 450,
child: ListTile(
title: const Text('Change tight factor'),
subtitle: Slider(
min: 0.1,
max: 4.0,
// divisions: ((4 - .1) * 400).floor(),
// label: _tightFactor.floor().toString(),
value: _tightFactor,
onChanged: (value) {
setState(() {
_tightFactor = value;
});
},
),
trailing: Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
const Text(
'Factor',
style: TextStyle(fontSize: 11),
),
Text(
_tightFactor.toStringAsFixed(2),
style: const TextStyle(fontSize: 15),
),
],
),
),
),
),
),
],
),
),
);
}
}
class GradientGlowButton extends StatelessWidget {
const GradientGlowButton({
Key key,
this.size,
this.onTap,
this.elevation = 0,
this.borderRadius = 0,
}) : super(key: key);
final double size;
final double elevation;
final double borderRadius;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: RaisedCard(
elevation: elevation,
borderRadius: borderRadius,
backgroundColor: Colors.red[900],
gradientColor: Colors.red[400],
shadowColor: Colors.red[700],
useGlowElevation: true,
onTap: () {},
child: Center(
child: Icon(
Icons.home,
size: size * 0.7,
color: Colors.white70,
),
),
),
);
}
}
class GradientGlowightClipperButton extends StatelessWidget {
const GradientGlowightClipperButton({
Key key,
this.size,
this.onTap,
this.elevation = 0,
this.borderRadius = 0,
this.tightFactor = 1.0,
}) : super(key: key);
final double size;
final VoidCallback onTap;
final double borderRadius;
final double elevation;
final double tightFactor;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: RaisedCardCustomClip(
elevation: elevation,
borderRadius: borderRadius,
backgroundColor: Colors.red[900],
gradientColor: Colors.red[400],
shadowColor: Colors.red[700],
useGlowElevation: true,
tightFactor: tightFactor,
onTap: () {},
child: Center(
child: Icon(
Icons.home,
size: size * 0.7,
color: Colors.white70,
),
),
),
);
}
}
class TightRRectClipper extends CustomClipper<RRect> {
TightRRectClipper(
this.devicePixelRatio, {
this.tightFactor = 1,
this.radius = 0,
});
final double devicePixelRatio;
final double tightFactor;
final double radius;
@override
RRect getClip(Size size) {
final double padding = 1 / devicePixelRatio * tightFactor;
return RRect.fromLTRBR(
padding,
padding,
size.width - padding,
size.height - padding,
Radius.circular(radius),
);
}
@override
bool shouldReclip(CustomClipper<RRect> oldClipper) {
return true;
}
}
// A convenience custom raised Material card widget. It uses standard
// Material card elevation if no shadow colors are provided and theme
// colors. By default the color scheme follows theme colors just like Material,
// but it can also use alternative shade color just like Material.
//
// The custom features are:
// * Glow elevation via flag [useGlowElevation]
// * Gradient from TopLeft background color to BottomRight gradient
// * Can swap gradient color order with [orderIsBackgroundToGradient]
// * Has circular rounded borders with [borderRadius]
class RaisedCard extends StatelessWidget {
const RaisedCard({
Key key,
@required this.child,
this.onTap,
this.elevation = 0,
this.useGlowElevation = false,
this.shadowColor,
this.backgroundColor,
this.borderRadius = 0,
this.useBorder = false,
this.gradientColor,
this.orderIsBackgroundToGradient = true,
this.borderColor,
}) : super(key: key);
final Widget child;
final VoidCallback onTap;
final double elevation;
/// If [useGlowElevation] is true, the shadow colors are used to make a custom
/// "glow" elevation with BoxShadow instead of the standard Material elevation.
final bool useGlowElevation;
final Color shadowColor;
/// Gradient is made with [backgroundColor] from upper left to lower right.
final Color backgroundColor;
// The gradient end color
final Color gradientColor;
/// If [orderIsBackgroundToGradient] is false, the gradient colors order
/// are switched so that we draw from [gradientColor] to [backgroundColor].
final bool orderIsBackgroundToGradient;
/// The Raised card can if so needed have rounded corners and 1 px outline
/// border. Border might be needed if card has same color as where it is
/// drawn.
final double borderRadius;
final bool useBorder;
final Color borderColor;
@override
Widget build(BuildContext context) {
// Assign default background and border colors from Theme if none given
final Color _backgroundColor =
backgroundColor ?? Theme.of(context).cardColor;
final Color _borderColor =
borderColor ?? Theme.of(context).scaffoldBackgroundColor;
// If no gradient color, then set to same color as the background
final Color _gradientColor = gradientColor ?? _backgroundColor;
Color _shadowColor = shadowColor;
if (useGlowElevation && _shadowColor == null) {
_shadowColor = const Color(0x80333333); // Grey if nothing was given
}
// Use the standard material shadow if no shadow at all yet given
_shadowColor ??= const Color(0xFF000000);
// We wrap Material in a Container so we can make a custom glow
// elevation as an alternative to the standard Material elevation.
// If the glow elevation is not used, we get a standard Material elevation.
return Container(
// This is for the custom glow like elevation
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
boxShadow: <BoxShadow>[
// Using background glow "elevation", instead of Material Elevation
// if elevation was null, it we set to 0, so no elevation
if (useGlowElevation && (elevation ?? 0) > 0)
BoxShadow(
color: _shadowColor,
offset: Offset(elevation / 1.3 + 1.5, elevation / 0.9 + 1),
blurRadius: elevation + 1,
spreadRadius: elevation / (elevation * 0.8) + 1,
),
],
),
child: Material(
type: MaterialType.card,
// Set Material Elevation only if we do not useGlowElevation
// if elevation was null, we set to 0, so no elevation
elevation: useGlowElevation ? 0 : elevation ?? 0,
shadowColor: _shadowColor,
borderRadius: BorderRadius.circular(borderRadius),
clipBehavior: Clip.antiAlias,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
border: useBorder ? Border.all(color: _borderColor) : null,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: orderIsBackgroundToGradient
? <Color>[_backgroundColor, _gradientColor]
: <Color>[_gradientColor, _backgroundColor],
),
),
// Using an extra transparent Material wrapper on an InkWell is a
// trick used to get Ink and Hover effects on colored or
// gradient background widgets
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: child,
),
),
),
),
);
}
}
class RaisedCardCustomClip extends StatelessWidget {
const RaisedCardCustomClip({
Key key,
@required this.child,
this.onTap,
this.elevation = 0,
this.useGlowElevation = false,
this.shadowColor,
this.backgroundColor,
this.borderRadius = 0,
this.useBorder = false,
this.gradientColor,
this.orderIsBackgroundToGradient = true,
this.borderColor,
this.tightFactor = 1.0,
}) : super(key: key);
final Widget child;
final VoidCallback onTap;
final double elevation;
/// If [useGlowElevation] is true, the shadow colors are used to make a custom
/// "glow" elevation with BoxShadow instead of the standard Material elevation.
final bool useGlowElevation;
final Color shadowColor;
/// Gradient is made with [backgroundColor] from upper left to lower right.
final Color backgroundColor;
// The gradient end color
final Color gradientColor;
/// If [orderIsBackgroundToGradient] is false, the gradient colors order
/// are switched so that we draw from [gradientColor] to [backgroundColor].
final bool orderIsBackgroundToGradient;
/// The Raised card can if so needed have rounded corners and 1 px outline
/// border. Border might be needed if card has same color as where it is
/// drawn.
final double borderRadius;
final bool useBorder;
final Color borderColor;
final double tightFactor;
@override
Widget build(BuildContext context) {
// Assign default background and border colors from Theme if none given
final Color _backgroundColor =
backgroundColor ?? Theme.of(context).cardColor;
final Color _borderColor =
borderColor ?? Theme.of(context).scaffoldBackgroundColor;
// If no gradient color, then set to same color as the background
final Color _gradientColor = gradientColor ?? _backgroundColor;
Color _shadowColor = shadowColor;
if (useGlowElevation && _shadowColor == null) {
_shadowColor = const Color(0x80333333); // Grey if nothing was given
}
// Use the standard material shadow if no shadow at all yet given
_shadowColor ??= const Color(0xFF000000);
// We wrap Material in a Container so we can make a custom glow
// elevation as an alternative to the standard Material elevation.
// If the glow elevation is not used, we get a standard Material elevation.
return Container(
// This is for the custom glow like elevation
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
boxShadow: <BoxShadow>[
// Using background glow "elevation", instead of Material Elevation
// if elevation was null, it we set to 0, so no elevation
if (useGlowElevation && (elevation ?? 0) > 0)
BoxShadow(
color: _shadowColor,
offset: Offset(elevation / 1.3 + 1.5, elevation / 0.9 + 1),
blurRadius: elevation + 1,
spreadRadius: elevation / (elevation * 0.8) + 1,
),
],
),
child: ClipRRect(
clipBehavior: Clip.antiAlias,
clipper: TightRRectClipper(
MediaQuery.of(context).devicePixelRatio,
tightFactor: tightFactor,
radius: borderRadius,
),
child: Material(
type: MaterialType.card,
// Set Material Elevation only if we do not useGlowElevation
// if elevation was null, we set to 0, so no elevation
elevation: useGlowElevation ? 0 : elevation ?? 0,
shadowColor: _shadowColor,
borderRadius: BorderRadius.circular(borderRadius),
clipBehavior: Clip.antiAlias,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
border: useBorder ? Border.all(color: _borderColor) : null,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: orderIsBackgroundToGradient
? <Color>[_backgroundColor, _gradientColor]
: <Color>[_gradientColor, _backgroundColor],
),
),
// Using an extra transparent Material wrapper on an InkWell is a
// trick used to get Ink and Hover effects on colored or
// gradient background widgets
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: child,
),
),
),
),
),
);
}
}
@rydmike
Copy link
Author

rydmike commented Jun 18, 2020

This is a demo case for this Flutter issue
flutter/flutter#58547

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