mirror of https://github.com/flutter/samples.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
429 lines
14 KiB
429 lines
14 KiB
// Copyright 2023 The Flutter team. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:math';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'components.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'dart:ui' as ui;
|
|
import '../model/puzzle_model.dart';
|
|
import '../page_content/pages_flow.dart';
|
|
|
|
class RotatorPuzzle extends StatefulWidget {
|
|
final PageConfig pageConfig;
|
|
final int numTiles;
|
|
final int puzzleNum;
|
|
final String shaderKey;
|
|
final int shaderDuration;
|
|
|
|
final String tileShadedString;
|
|
final double tileShadedStringSize;
|
|
final EdgeInsets tileShadedStringPadding;
|
|
final int tileShadedStringAnimDuration;
|
|
final List<WonkyAnimSetting> tileShadedStringAnimSettings;
|
|
final double tileScaleModifier;
|
|
|
|
const RotatorPuzzle({
|
|
Key? key,
|
|
required this.pageConfig,
|
|
required this.numTiles,
|
|
required this.puzzleNum,
|
|
required this.shaderKey,
|
|
required this.shaderDuration,
|
|
required this.tileShadedString,
|
|
required this.tileShadedStringSize,
|
|
required this.tileShadedStringPadding,
|
|
required this.tileShadedStringAnimDuration,
|
|
this.tileShadedStringAnimSettings = const [],
|
|
this.tileScaleModifier = 1.0,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<RotatorPuzzle> createState() => RotatorPuzzleState();
|
|
}
|
|
|
|
class RotatorPuzzleState extends State<RotatorPuzzle>
|
|
with TickerProviderStateMixin {
|
|
late PuzzleModel puzzleModel;
|
|
bool solved = false;
|
|
late final AnimationController animationController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1000),
|
|
);
|
|
late final CurvedAnimation animationCurve = CurvedAnimation(
|
|
parent: animationController,
|
|
curve: const Interval(
|
|
0.2,
|
|
0.45,
|
|
curve: Curves.easeOut,
|
|
),
|
|
);
|
|
late Animation<double> opacAnimation =
|
|
Tween<double>(begin: 0.4, end: 1.0).animate(animationCurve)
|
|
..addListener(() {
|
|
setState(() {});
|
|
});
|
|
|
|
List<GlobalKey<RotatorPuzzleTileState>> tileKeys = [];
|
|
GlobalKey<FragmentShadedState> shadedWidgetStackHackStateKey = GlobalKey();
|
|
GlobalKey shadedWidgetRepaintBoundaryKey = GlobalKey();
|
|
GlobalKey<WonkyCharState> tileBgWonkyCharKey = GlobalKey();
|
|
ui.Image? shadedImg;
|
|
|
|
@override
|
|
void initState() {
|
|
for (int i = 0; i < widget.numTiles; i++) {
|
|
tileKeys.add(GlobalKey<RotatorPuzzleTileState>());
|
|
}
|
|
puzzleModel = PuzzleModel(
|
|
dim: widget.numTiles,
|
|
); //TODO check if correct; correlate dim and numTiles; probably get rid of numTiles
|
|
generateTiles();
|
|
shuffle();
|
|
super.initState();
|
|
}
|
|
|
|
List<RotatorPuzzleTile> generateTiles() {
|
|
// TODO move to build?
|
|
List<RotatorPuzzleTile> tiles = [];
|
|
int dim = sqrt(widget.numTiles).round();
|
|
for (int i = 0; i < widget.numTiles; i++) {
|
|
RotatorPuzzleTile tile = RotatorPuzzleTile(
|
|
key: tileKeys[i],
|
|
tileID: i,
|
|
row: (i / dim).floor(),
|
|
col: i % dim,
|
|
parentState: this,
|
|
shaderKey: widget.shaderKey,
|
|
shaderDuration: widget.shaderDuration,
|
|
tileShadedString: widget.tileShadedString,
|
|
tileShadedStringSize: widget.tileShadedStringSize,
|
|
tileShadedStringPadding: widget.tileShadedStringPadding,
|
|
animationSettings: widget.tileShadedStringAnimSettings,
|
|
tileShadedStringAnimDuration: widget.tileShadedStringAnimDuration,
|
|
tileScaleModifier: widget.tileScaleModifier,
|
|
);
|
|
tiles.add(tile);
|
|
}
|
|
return tiles;
|
|
}
|
|
|
|
void handlePointerDown({required int tileID}) {
|
|
puzzleModel.rotateTile(tileID);
|
|
if (puzzleModel.allRotationsCorrect()) {
|
|
handleSolved();
|
|
}
|
|
}
|
|
|
|
void handleSolved() {
|
|
animationController.addStatusListener((status) {
|
|
solved = true;
|
|
for (GlobalKey<RotatorPuzzleTileState> k in tileKeys) {
|
|
if (null != k.currentState && k.currentState!.mounted) {
|
|
startDampening();
|
|
tileBgWonkyCharKey.currentState!.stopAnimation();
|
|
}
|
|
}
|
|
if (status == AnimationStatus.completed) {
|
|
Future.delayed(
|
|
const Duration(milliseconds: FragmentShaded.dampenDuration + 250),
|
|
() {
|
|
widget.pageConfig.pageController.nextPage(
|
|
duration:
|
|
const Duration(milliseconds: PagesFlow.pageScrollDuration),
|
|
curve: Curves.easeOut,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
animationController.forward();
|
|
}
|
|
|
|
void shuffle() {
|
|
Random rng = Random(0xC00010FF);
|
|
for (int i = 0; i < widget.numTiles; i++) {
|
|
int rando = rng.nextInt(3);
|
|
puzzleModel.setTileStatus(i, rando);
|
|
if (puzzleModel.allRotationsCorrect()) {
|
|
// fallback to prevent starting on solved puzzle
|
|
puzzleModel.setTileStatus(0, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
double tileSize() {
|
|
return widget.pageConfig.puzzleSize / sqrt(widget.numTiles);
|
|
}
|
|
|
|
List<double> tileCoords({required int row, required int col}) {
|
|
return <double>[col * tileSize(), row * tileSize()];
|
|
}
|
|
|
|
void setImageFromRepaintBoundary(GlobalKey which) {
|
|
final BuildContext? context = which.currentContext;
|
|
if (null != context) {
|
|
final RenderRepaintBoundary boundary =
|
|
context.findRenderObject()! as RenderRepaintBoundary;
|
|
final ui.Image img = boundary.toImageSync();
|
|
if (mounted) {
|
|
setState(() {
|
|
shadedImg = img;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void startDampening() {
|
|
if (null != shadedWidgetStackHackStateKey.currentState &&
|
|
shadedWidgetStackHackStateKey.currentState!.mounted) {
|
|
shadedWidgetStackHackStateKey.currentState!.startDampening();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// TODO fix widget implementation to remove the need for this hack
|
|
// to force a setState rebuild
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
// end hack ----------------
|
|
setImageFromRepaintBoundary(shadedWidgetRepaintBoundaryKey);
|
|
return Center(
|
|
child: SizedBox(
|
|
width: widget.pageConfig.puzzleSize,
|
|
height: widget.pageConfig.puzzleSize,
|
|
child: Opacity(
|
|
opacity: opacAnimation.value,
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Positioned(
|
|
left: -9999,
|
|
top: -9999,
|
|
child: RepaintBoundary(
|
|
key: shadedWidgetRepaintBoundaryKey,
|
|
child: SizedBox(
|
|
width: widget.pageConfig.puzzleSize * 4,
|
|
height: widget.pageConfig.puzzleSize * 4,
|
|
child: Center(
|
|
child: FragmentShaded(
|
|
key: shadedWidgetStackHackStateKey,
|
|
shaderName: widget.shaderKey,
|
|
shaderDuration: widget.shaderDuration,
|
|
child: Padding(
|
|
padding: widget.tileShadedStringPadding,
|
|
child: WonkyChar(
|
|
key: tileBgWonkyCharKey,
|
|
text: widget.tileShadedString,
|
|
size: widget.tileShadedStringSize,
|
|
animDurationMillis:
|
|
widget.tileShadedStringAnimDuration,
|
|
animationSettings:
|
|
widget.tileShadedStringAnimSettings,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
] +
|
|
generateTiles(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
class RotatorPuzzleTile extends StatefulWidget {
|
|
final int tileID;
|
|
final RotatorPuzzleState parentState;
|
|
final String shaderKey;
|
|
final int shaderDuration;
|
|
final String tileShadedString;
|
|
final double tileShadedStringSize;
|
|
final EdgeInsets tileShadedStringPadding;
|
|
final int tileShadedStringAnimDuration;
|
|
final List<WonkyAnimSetting> animationSettings;
|
|
final double tileScaleModifier;
|
|
|
|
// TODO get row/col out into model
|
|
final int row;
|
|
final int col;
|
|
|
|
RotatorPuzzleTile({
|
|
Key? key,
|
|
required this.tileID,
|
|
required this.row,
|
|
required this.col,
|
|
required this.parentState,
|
|
required this.shaderKey,
|
|
required this.shaderDuration,
|
|
required this.tileShadedString,
|
|
required this.tileShadedStringSize,
|
|
required this.tileShadedStringPadding,
|
|
required this.animationSettings,
|
|
required this.tileShadedStringAnimDuration,
|
|
required this.tileScaleModifier,
|
|
}) : super(key: key);
|
|
|
|
final State<RotatorPuzzleTile> tileState = RotatorPuzzleTileState();
|
|
|
|
@override
|
|
State<RotatorPuzzleTile> createState() => RotatorPuzzleTileState();
|
|
}
|
|
|
|
class RotatorPuzzleTileState extends State<RotatorPuzzleTile>
|
|
with TickerProviderStateMixin {
|
|
double touchedOpac = 0.0;
|
|
Duration touchedOpacDur = const Duration(milliseconds: 50);
|
|
late final AnimationController animationController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(
|
|
milliseconds: 100,
|
|
),
|
|
);
|
|
late final CurvedAnimation animationCurve = CurvedAnimation(
|
|
parent: animationController,
|
|
curve: Curves.easeOut,
|
|
);
|
|
late Animation<double> animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
animation = Tween<double>(
|
|
// initialize animation to starting point
|
|
begin: currentStatus() * pi * 0.5,
|
|
end: currentStatus() * pi * 0.5,
|
|
).animate(animationController);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// TODO fix widget implementation to remove the need for this hack
|
|
// to force a setState rebuild
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
// end hack ------------------------------
|
|
List<double> coords =
|
|
widget.parentState.tileCoords(row: widget.row, col: widget.col);
|
|
double zeroPoint = widget.parentState.widget.pageConfig.puzzleSize * .5 -
|
|
widget.parentState.tileSize() * 0.5;
|
|
|
|
return Stack(
|
|
children: [
|
|
Stack(
|
|
children: [
|
|
Positioned(
|
|
left: coords[0],
|
|
top: coords[1],
|
|
child: Transform(
|
|
transform: Matrix4.rotationZ(animation.value),
|
|
alignment: Alignment.center,
|
|
child: GestureDetector(
|
|
onTap: handlePointerDown,
|
|
child: ClipRect(
|
|
child: SizedBox(
|
|
width: widget.parentState.tileSize(),
|
|
height: widget.parentState.tileSize(),
|
|
child: OverflowBox(
|
|
maxHeight:
|
|
widget.parentState.widget.pageConfig.puzzleSize,
|
|
maxWidth:
|
|
widget.parentState.widget.pageConfig.puzzleSize,
|
|
child: Transform.translate(
|
|
offset: Offset(
|
|
zeroPoint -
|
|
widget.col * widget.parentState.tileSize(),
|
|
zeroPoint -
|
|
widget.row * widget.parentState.tileSize(),
|
|
),
|
|
child: SizedBox(
|
|
width:
|
|
widget.parentState.widget.pageConfig.puzzleSize,
|
|
height:
|
|
widget.parentState.widget.pageConfig.puzzleSize,
|
|
child: Transform.scale(
|
|
scale: widget.tileScaleModifier,
|
|
child: RawImage(
|
|
image: widget.parentState.shadedImg,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// puzzle tile overlay fades in/out on tap, to indicate touched tile
|
|
Positioned(
|
|
left: coords[0],
|
|
top: coords[1],
|
|
child: IgnorePointer(
|
|
child: AnimatedOpacity(
|
|
opacity: touchedOpac,
|
|
duration: touchedOpacDur,
|
|
onEnd: () {
|
|
if (touchedOpac == 1.0) {
|
|
touchedOpac = 0.0;
|
|
touchedOpacDur = const Duration(milliseconds: 300);
|
|
setState(() {});
|
|
}
|
|
},
|
|
child: DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
color: Color.fromARGB(120, 0, 0, 0)),
|
|
child: SizedBox(
|
|
width: widget.parentState.tileSize(),
|
|
height: widget.parentState.tileSize(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void handlePointerDown() {
|
|
if (!widget.parentState.solved) {
|
|
int oldStatus = currentStatus();
|
|
widget.parentState.handlePointerDown(tileID: widget.tileID);
|
|
touchedOpac = 1.0;
|
|
touchedOpacDur = const Duration(milliseconds: 100);
|
|
rotateTile(oldStatus: oldStatus);
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
int currentStatus() {
|
|
return widget.parentState.puzzleModel.getTileStatus(widget.tileID);
|
|
}
|
|
|
|
void rotateTile({required int oldStatus}) {
|
|
animation = Tween<double>(
|
|
begin: oldStatus * pi * 0.5,
|
|
end: currentStatus() * pi * 0.5,
|
|
).animate(animationController)
|
|
..addListener(() {
|
|
setState(() {});
|
|
});
|
|
animationController.reset();
|
|
animationController.forward();
|
|
}
|
|
}
|