Skip to content

Instantly share code, notes, and snippets.

@ChrisMarxDev
Created July 22, 2023 08:41
Show Gist options
  • Save ChrisMarxDev/99400378687bae5a5b64a34c000eea50 to your computer and use it in GitHub Desktop.
Save ChrisMarxDev/99400378687bae5a5b64a34c000eea50 to your computer and use it in GitHub Desktop.
Bouncy Card Widget
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