redesign: duolingo-pattern onboarding, no gradient, pressable button
- Nunito font replacing Plus Jakarta Sans - SButton: Stack-based 3D pressable (dark shadow layer + top layer translates on press) - SCard: flat border-based selection, no shadows - SProgressBar: 4px thin bar, animated fill - All screens: white bg, no gradients - Duolingo layout: illustration top zone, content below, sticky button
This commit is contained in:
88
lib/widgets/s_button.dart
Normal file
88
lib/widgets/s_button.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../theme.dart';
|
||||
|
||||
// Duolingo-style pressable button.
|
||||
// Stack of two containers: dark "shadow" bottom + colored top that translates down on press.
|
||||
class SButton extends StatefulWidget {
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
final bool enabled;
|
||||
final Color? color;
|
||||
final Color? shadowColor;
|
||||
final Color? textColor;
|
||||
|
||||
const SButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.enabled = true,
|
||||
this.color,
|
||||
this.shadowColor,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SButton> createState() => _SButtonState();
|
||||
}
|
||||
|
||||
class _SButtonState extends State<SButton> {
|
||||
bool _pressed = false;
|
||||
|
||||
bool get _active => widget.enabled && widget.onTap != null;
|
||||
|
||||
static const _shadowH = 4.0;
|
||||
static const _btnH = 52.0;
|
||||
static const _radius = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bg = _active ? (widget.color ?? SColors.primary) : const Color(0xFFE5E5E5);
|
||||
final shadow = _active ? (widget.shadowColor ?? SColors.primaryDark) : const Color(0xFFB8B8B8);
|
||||
final textCol = _active ? (widget.textColor ?? Colors.white) : SColors.textDisabled;
|
||||
final offset = (_pressed && _active) ? _shadowH : 0.0;
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: (_) { if (_active) setState(() => _pressed = true); },
|
||||
onTapUp: (_) {
|
||||
if (!_active) return;
|
||||
setState(() => _pressed = false);
|
||||
HapticFeedback.mediumImpact();
|
||||
widget.onTap!();
|
||||
},
|
||||
onTapCancel: () { if (_active) setState(() => _pressed = false); },
|
||||
child: SizedBox(
|
||||
height: _btnH + _shadowH,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Shadow layer (always at bottom)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: shadow,
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Button layer (translates down on press, "sinking" into shadow)
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 80),
|
||||
top: offset,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: _btnH,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(widget.label, style: SText.button(textCol)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user