170 lines
6.7 KiB
Dart
170 lines
6.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
class SlideToUnlock extends StatefulWidget {
|
|
final VoidCallback onUnlocked;
|
|
final String lockedText;
|
|
final String unlockedText;
|
|
final IconData lockedIcon;
|
|
final IconData unlockedIcon;
|
|
final bool isLocked;
|
|
final double? height;
|
|
final double? thumbSize;
|
|
final Color? backgroundColor;
|
|
final Color? accentColor;
|
|
|
|
const SlideToUnlock({
|
|
super.key,
|
|
required this.onUnlocked,
|
|
this.lockedText = "スライドして解除",
|
|
this.unlockedText = "UNLOCKED",
|
|
this.lockedIcon = Icons.lock,
|
|
this.unlockedIcon = Icons.check_circle,
|
|
this.isLocked = true,
|
|
this.height = 72,
|
|
this.thumbSize = 52,
|
|
this.backgroundColor,
|
|
this.accentColor,
|
|
});
|
|
|
|
@override
|
|
State<SlideToUnlock> createState() => _SlideToUnlockState();
|
|
}
|
|
|
|
class _SlideToUnlockState extends State<SlideToUnlock> {
|
|
double _position = 0.0;
|
|
static const double _trackPadding = 14.0;
|
|
bool _showSuccessOverlay = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!widget.isLocked) return const SizedBox.shrink();
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final double maxWidth = constraints.maxWidth;
|
|
final double thumbSize = widget.thumbSize ?? 52;
|
|
final double trackWidth = (maxWidth - thumbSize - (_trackPadding * 2)).clamp(0, maxWidth);
|
|
final double progressRatio = trackWidth == 0 ? 0 : (_position / trackWidth).clamp(0, 1);
|
|
final double innerWidth = thumbSize + trackWidth;
|
|
final double progressWidth = (innerWidth * progressRatio + thumbSize * (1 - progressRatio)).clamp(thumbSize, innerWidth);
|
|
final Color background = widget.backgroundColor ?? Colors.blueGrey.shade900;
|
|
final Color accentStart = (widget.accentColor ?? Colors.indigo.shade600).withValues(alpha: 0.9);
|
|
final Color accentEnd = (widget.accentColor ?? Colors.indigo.shade600);
|
|
|
|
return Container(
|
|
height: widget.height ?? 72,
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: background,
|
|
borderRadius: BorderRadius.circular(32),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.black26, blurRadius: 8, offset: const Offset(0, 4)),
|
|
],
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
// 進行バー
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: _trackPadding, vertical: 12),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 150),
|
|
curve: Curves.easeOut,
|
|
width: progressWidth,
|
|
height: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(28),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
accentStart,
|
|
accentEnd,
|
|
],
|
|
),
|
|
boxShadow: const [
|
|
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// 背景テキスト
|
|
Center(
|
|
child: AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 200),
|
|
child: _showSuccessOverlay
|
|
? Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
key: const ValueKey('unlocked'),
|
|
children: [
|
|
Icon(widget.unlockedIcon, color: Colors.white, size: 24),
|
|
const SizedBox(width: 6),
|
|
Text(widget.unlockedText, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
],
|
|
)
|
|
: Row(
|
|
key: const ValueKey('locked'),
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(widget.lockedIcon, color: Colors.white.withValues(alpha: 0.85), size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
widget.lockedText,
|
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 1.1),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// スライドつまみ
|
|
Positioned(
|
|
left: _trackPadding + _position,
|
|
top: ((widget.height ?? 72) - (widget.thumbSize ?? 52)) / 2,
|
|
child: GestureDetector(
|
|
onHorizontalDragUpdate: (details) {
|
|
setState(() {
|
|
_position += details.delta.dx;
|
|
if (_position < 0) _position = 0;
|
|
if (_position > trackWidth) _position = trackWidth;
|
|
});
|
|
},
|
|
onHorizontalDragEnd: (details) {
|
|
if (_position >= trackWidth * 0.65) {
|
|
setState(() {
|
|
_position = trackWidth;
|
|
_showSuccessOverlay = true;
|
|
});
|
|
widget.onUnlocked();
|
|
Future.delayed(const Duration(milliseconds: 450), () {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_position = 0;
|
|
_showSuccessOverlay = false;
|
|
});
|
|
});
|
|
} else {
|
|
// 失敗時はバネのように戻る(簡易)
|
|
setState(() => _position = 0);
|
|
}
|
|
},
|
|
child: Container(
|
|
width: thumbSize,
|
|
height: widget.thumbSize ?? 52,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular((widget.thumbSize ?? 52) / 2),
|
|
boxShadow: const [
|
|
BoxShadow(color: Colors.black38, blurRadius: 8, offset: Offset(0, 4)),
|
|
],
|
|
),
|
|
child: Icon(Icons.arrow_forward_ios, color: accentEnd, size: 20),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|