mirror of
https://github.com/Linloir/Flutter-Wordle.git
synced 2025-12-17 06:18:12 +08:00
363 lines
13 KiB
Dart
363 lines
13 KiB
Dart
/*
|
|
* @Author : Linloir
|
|
* @Date : 2022-03-09 22:33:41
|
|
* @LastEditTime : 2022-03-11 14:01:14
|
|
* @Description : Single Selection Box
|
|
*/
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'dart:math';
|
|
import './group_shared.dart';
|
|
|
|
class SingleSelectionBox extends StatefulWidget {
|
|
const SingleSelectionBox({
|
|
Key? key,
|
|
required this.id,
|
|
required this.unselectedChild,
|
|
required this.selectedChild,
|
|
//These two are displayed by animation widget so not include in the decoration parameter
|
|
required this.selectedBackgroundColor,
|
|
required this.unselectedBackgroundColor,
|
|
this.shadowBlurRadius = 8,
|
|
this.shadowSpreadRadius = 0,
|
|
this.unselectedShadowColor = Colors.transparent,
|
|
this.selectedShadowColor = Colors.transparent,
|
|
this.borderRadius = 0.0,
|
|
this.borderColor = Colors.transparent,
|
|
this.borderWidth = 0.0,
|
|
}):
|
|
super(key: key);
|
|
|
|
final int id;
|
|
final Widget unselectedChild;
|
|
final Widget selectedChild;
|
|
final Color selectedBackgroundColor;
|
|
final Color unselectedBackgroundColor;
|
|
final double shadowBlurRadius;
|
|
final double shadowSpreadRadius;
|
|
final Color unselectedShadowColor;
|
|
final Color selectedShadowColor;
|
|
final double borderRadius;
|
|
final Color borderColor;
|
|
final double borderWidth;
|
|
|
|
@override
|
|
State<SingleSelectionBox> createState() => _SingleSelectionBoxState();
|
|
}
|
|
|
|
class _SingleSelectionBoxState extends State<SingleSelectionBox> with TickerProviderStateMixin{
|
|
bool selected = false;
|
|
|
|
//Record last pressed shift from center point
|
|
double pressedX = 0;
|
|
double pressedY = 0;
|
|
|
|
//Grow animation controller
|
|
late final AnimationController _scaleController;
|
|
late final AnimationController _opacController;
|
|
late final AnimationController _shrinkController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_scaleController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 750),
|
|
);
|
|
_opacController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 750),
|
|
);
|
|
_shrinkController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 500),
|
|
);
|
|
if(context.findAncestorWidgetOfExactType<GroupSharedData>()!.selected == widget.id) {
|
|
_scaleController.value = _scaleController.upperBound;
|
|
selected = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
var newData = context.findAncestorWidgetOfExactType<GroupSharedData>()!;
|
|
if(newData.selected != widget.id && selected) {
|
|
_opacController.forward();
|
|
selected = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var data = GroupSharedData.of(context)!;
|
|
//Use gesture detector to detect the position pressed for animations
|
|
return LayoutBuilder(
|
|
builder:(context, constraints) {
|
|
var decoration = BoxDecoration(
|
|
boxShadow: [BoxShadow(
|
|
color: data.selected == widget.id ? widget.selectedShadowColor : widget.unselectedShadowColor,
|
|
blurRadius: widget.shadowBlurRadius,
|
|
spreadRadius: widget.shadowSpreadRadius,
|
|
)],
|
|
color: widget.unselectedBackgroundColor,
|
|
border: widget.borderWidth > 0 ? Border.all(
|
|
color: data.selected == widget.id ? widget.selectedBackgroundColor : widget.borderColor,
|
|
width: widget.borderWidth
|
|
) : null,
|
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
|
);
|
|
return GestureDetector(
|
|
//Stack best describes the relation between the animation and the container box
|
|
child: AnimatedBuilder(
|
|
animation: _shrinkController,
|
|
builder: (context, child) {
|
|
var _shrinkAnimation = Tween<double>(begin: 1, end: 0.9).animate(CurvedAnimation(
|
|
parent: _shrinkController,
|
|
curve: Curves.elasticOut,
|
|
reverseCurve: Curves.elasticIn,
|
|
));
|
|
return Transform.scale(
|
|
scale: _shrinkAnimation.value,
|
|
alignment: Alignment.center,
|
|
child: child,
|
|
);
|
|
},
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
//Container
|
|
Positioned.fill(
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 750),
|
|
decoration: decoration,
|
|
alignment: Alignment.center,
|
|
),
|
|
),
|
|
//Animation ( similar to ink well effect )
|
|
Positioned.fill(
|
|
child: AnimatedBuilder(
|
|
animation: _scaleController,
|
|
child: AnimatedBuilder(
|
|
animation: _opacController,
|
|
builder: (context, child) {
|
|
num topLeftSquredDis = pow(pressedX, 2) + pow(pressedY, 2);
|
|
num topRightSquredDis = pow(constraints.maxWidth - pressedX, 2) + pow(pressedY, 2);
|
|
num botLeftSquredDis = pow(pressedX, 2) + pow(constraints.maxHeight - pressedY, 2);
|
|
num botRightSquredDis = pow(constraints.maxWidth - pressedX, 2) + pow(constraints.maxHeight - pressedY, 2);
|
|
num radius = sqrt(max(max(topLeftSquredDis, topRightSquredDis), max(botLeftSquredDis, botRightSquredDis)));
|
|
var _opacityAnimation = Tween<double>(begin: 1, end: 0).animate(_opacController);
|
|
return OverflowBox(
|
|
maxHeight: radius * 2,
|
|
maxWidth: radius * 2,
|
|
child: SizedBox(
|
|
child: Opacity(
|
|
opacity: _opacityAnimation.value,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: widget.selectedBackgroundColor,
|
|
),
|
|
),
|
|
),
|
|
width: radius * 2,
|
|
height: radius * 2,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
builder: (context, child) {
|
|
var _scaleAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
|
|
parent: _scaleController,
|
|
curve: Curves.easeInOutCubic,
|
|
));
|
|
return ClipRRect(
|
|
child: Transform.translate(
|
|
child: Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: child,
|
|
),
|
|
offset: Offset(pressedX - constraints.maxWidth / 2, pressedY - constraints.maxHeight / 2),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
|
);
|
|
},
|
|
)
|
|
),
|
|
Positioned(
|
|
child: AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 750),
|
|
child: data.selected == widget.id ? widget.selectedChild : widget.unselectedChild,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
onTapUp: (detail) {
|
|
setState(() {
|
|
selected = true;
|
|
//Notify the group manager
|
|
SelectNotification(selection: widget.id).dispatch(context);
|
|
//Record the coordinate and start the animation
|
|
pressedX = detail.localPosition.dx;
|
|
pressedY = detail.localPosition.dy;
|
|
_opacController.reset();
|
|
_scaleController.reset();
|
|
_scaleController.forward();
|
|
_shrinkController.reverse();
|
|
});
|
|
},
|
|
onTapDown: ((detail) {
|
|
_shrinkController.value = _shrinkController.upperBound;
|
|
}),
|
|
onTapCancel: (() {
|
|
_shrinkController.reverse();
|
|
}),
|
|
);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget generateSelectionBox({
|
|
required int id,
|
|
required double width,
|
|
required double height,
|
|
required Color color,
|
|
required String primaryText,
|
|
required double primaryTextSize,
|
|
String? secondaryText,
|
|
double? secondaryTextSize,
|
|
String? decorationText,
|
|
Alignment alignment = Alignment.center,
|
|
EdgeInsets padding = EdgeInsets.zero,
|
|
}) {
|
|
return Container(
|
|
width: width,
|
|
height: height,
|
|
padding: const EdgeInsets.all(10.0),
|
|
child: SingleSelectionBox(
|
|
id: id,
|
|
unselectedBackgroundColor: Colors.white.withOpacity(0.6),
|
|
selectedBackgroundColor: color,
|
|
//unselectedShadowColor: Colors.grey[400]!,
|
|
//selectedShadowColor: color,
|
|
borderRadius: 10.0,
|
|
borderWidth: 0.0,
|
|
unselectedChild: Stack(
|
|
key: const ValueKey(0),
|
|
children: [
|
|
Positioned.fill(
|
|
child: Align(
|
|
alignment: alignment,
|
|
child: Padding(
|
|
padding: padding,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
|
child: Text(primaryText, style: TextStyle(
|
|
color: Colors.grey[850],
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: primaryTextSize,
|
|
),),
|
|
),
|
|
if(secondaryText != null)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
|
child: Text(secondaryText, style: TextStyle(
|
|
color: Colors.grey[850]!.withOpacity(0.8),
|
|
fontSize: secondaryTextSize,
|
|
),),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if(decorationText != null)
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
child: Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: Transform.translate(
|
|
offset: const Offset(15, 35),
|
|
child: Text(
|
|
decorationText,
|
|
style: TextStyle(
|
|
color: color.withOpacity(0.3),
|
|
fontSize: 120.0,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
)
|
|
),
|
|
),
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
)
|
|
)
|
|
],
|
|
),
|
|
selectedChild: Stack(
|
|
key: const ValueKey(1),
|
|
children: [
|
|
Positioned.fill(
|
|
child: Align(
|
|
alignment: alignment,
|
|
child: Padding(
|
|
padding: padding,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
|
child: Text(primaryText, style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: primaryTextSize,
|
|
),),
|
|
),
|
|
if(secondaryText != null)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
|
child: Text(secondaryText, style: TextStyle(
|
|
color: Colors.white.withOpacity(0.8),
|
|
fontSize: secondaryTextSize,
|
|
),),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if(decorationText != null)
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
child: Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: Transform.translate(
|
|
offset: const Offset(15, 35),
|
|
child: Text(
|
|
decorationText,
|
|
style: TextStyle(
|
|
color: Colors.white.withOpacity(0.3),
|
|
fontSize: 120.0,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
)
|
|
),
|
|
),
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
)
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|