Last active
June 18, 2020 15:59
-
-
Save rydmike/114c36d75084ab57716faea36993b8ce to your computer and use it in GitHub Desktop.
Flutter Material clipping issue with shadows
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 '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, | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a demo case for this Flutter issue
flutter/flutter#58547