Created
July 22, 2023 08:41
-
-
Save ChrisMarxDev/99400378687bae5a5b64a34c000eea50 to your computer and use it in GitHub Desktop.
Bouncy Card Widget
This file contains hidden or 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/material.dart'; | |
// I will have to clean this a little bit, I pasted my context_extension below so it works out of the box | |
class InteractableCard extends StatefulWidget { | |
const InteractableCard({ | |
required this.child, | |
this.highlightColor, | |
super.key, | |
this.onTap, | |
this.unselectedColor, | |
this.hoverColor, | |
this.selected = false, | |
this.elevation = 8, | |
this.distance, | |
}) : assert(elevation >= 0, 'elevation must be positive'); | |
final Widget child; | |
final void Function()? onTap; | |
final Color? hoverColor; | |
final bool selected; | |
final Color? highlightColor; | |
final Color? unselectedColor; | |
final double elevation; | |
final double? distance; | |
@override | |
State<InteractableCard> createState() => _InteractableCardState(); | |
} | |
class _InteractableCardState extends State<InteractableCard> { | |
bool hovered = false; | |
bool pressed = false; | |
@override | |
Widget build(BuildContext context) { | |
final highlightColor = widget.highlightColor ?? context.primary; | |
final hoverColor = widget.hoverColor ?? | |
Color.alphaBlend( | |
highlightColor.withOpacity(0.4), | |
context.cardColor, | |
); | |
final color = hovered || pressed && !widget.selected | |
? hoverColor | |
: widget.selected | |
? highlightColor | |
: context.cardColor; | |
final distanceOnPressed = | |
widget.distance?.clamp(0, widget.elevation).toDouble() ?? | |
widget.elevation / 2; | |
final marginShadow = | |
pressed ? widget.elevation - distanceOnPressed : widget.elevation; | |
final marginArea = pressed ? distanceOnPressed : 0.0; | |
return AnimatedContainer( | |
margin: EdgeInsets.only( | |
bottom: marginShadow, | |
right: marginShadow, | |
top: marginArea, | |
left: marginArea, | |
), | |
duration: kDurationQuick, | |
decoration: BoxDecoration( | |
color: color, | |
borderRadius: kBorderRadius, | |
border: kBorder, | |
boxShadow: [ | |
BoxShadow( | |
offset: Offset(widget.elevation, widget.elevation), | |
).pressed(pressed, distanceOnPressed) | |
], | |
), | |
child: InkWell( | |
onTapDown: (_) { | |
setState(() { | |
pressed = true; | |
}); | |
}, | |
onTapUp: (_) { | |
setState(() { | |
pressed = false; | |
}); | |
}, | |
onTapCancel: () { | |
setState(() { | |
pressed = false; | |
}); | |
}, | |
borderRadius: kBorderRadius, | |
splashColor: hoverColor, | |
onHover: (value) { | |
setState(() { | |
hovered = value; | |
}); | |
}, | |
onTap: widget.onTap, | |
child: Padding( | |
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), | |
child: widget.child, | |
), | |
), | |
); | |
} | |
} | |
const kPink = Color(0xFFEA439B); | |
const kTeal = Color(0xFF64D9DB); | |
const BorderRadius kBorderRadius = BorderRadius.all(Radius.circular(8)); | |
const Border kBorder = Border.fromBorderSide(BorderSide()); | |
const BoxShadow kShadowSmall = BoxShadow( | |
offset: Offset(4, 4), | |
); | |
const BoxShadow kShadowMedium = BoxShadow( | |
offset: Offset(4, 4), | |
); | |
const BoxShadow kShadowLarge = BoxShadow( | |
offset: Offset(8, 8), | |
); | |
const BoxDecoration kBaseBox = BoxDecoration( | |
borderRadius: kBorderRadius, | |
border: kBorder, | |
boxShadow: [ | |
kShadowLarge, | |
], | |
); | |
const kDurationBase = Duration(milliseconds: 300); | |
const kDurationQuick = Duration(milliseconds: 200); | |
extension ThemeExtension on BuildContext { | |
Color get primary => Theme.of(this).colorScheme.primary; | |
Color get onPrimary => Theme.of(this).colorScheme.onPrimary; | |
Color get secondary => Theme.of(this).colorScheme.secondary; | |
Color get onSecondary => Theme.of(this).colorScheme.onSecondary; | |
Color get background => Theme.of(this).colorScheme.background; | |
Color get onBackground => Theme.of(this).colorScheme.onBackground; | |
Color get cardColor => Theme.of(this).cardTheme.color ?? background; | |
Color get textColor => Theme.of(this).textTheme.bodyMedium!.color!; | |
bool get isDark => Theme.of(this).brightness == Brightness.dark; | |
// Color get weakGrey => isDark ? kWeakGreyDark : kWeakGreyLight; | |
TextStyle get bodySmall => Theme.of(this).textTheme.bodySmall!; | |
TextStyle get bodyMedium => Theme.of(this).textTheme.bodyMedium!; | |
TextStyle get bodyLarge => Theme.of(this).textTheme.bodyLarge!; | |
TextStyle get headlineSmall => Theme.of(this).textTheme.headlineSmall!; | |
TextStyle get headlineMedium => Theme.of(this).textTheme.headlineMedium!; | |
TextStyle get headlineLarge => Theme.of(this).textTheme.headlineLarge!; | |
TextStyle get titleSmall => Theme.of(this).textTheme.titleSmall!; | |
TextStyle get titleMedium => Theme.of(this).textTheme.titleMedium!; | |
TextStyle get titleLarge => Theme.of(this).textTheme.titleLarge!; | |
TextStyle get captionSmall => Theme.of(this).textTheme.labelSmall!; | |
TextStyle get captionMedium => Theme.of(this).textTheme.labelMedium!; | |
TextStyle get captionLarge => Theme.of(this).textTheme.labelLarge!; | |
} | |
extension ShadowExtension on BoxShadow { | |
// ignore: avoid_positional_boolean_parameters | |
BoxShadow pressed(bool pressed, double height) { | |
if (!pressed) return this; | |
return copyWith( | |
offset: Offset(offset.dx - height, offset.dy - height), | |
); | |
} | |
BoxShadow copyWith({ | |
Color? color, | |
Offset? offset, | |
double? blurRadius, | |
double? spreadRadius, | |
}) { | |
return BoxShadow( | |
color: color ?? this.color, | |
offset: offset ?? this.offset, | |
blurRadius: blurRadius ?? this.blurRadius, | |
spreadRadius: spreadRadius ?? this.spreadRadius, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment