// Copyright 2019 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 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/gallery_localizations.dart'; import 'transformations_demo_board.dart'; import 'transformations_demo_edit_board_point.dart'; // BEGIN transformationsDemo#1 class TransformationsDemo extends StatefulWidget { const TransformationsDemo({super.key}); @override State createState() => _TransformationsDemoState(); } class _TransformationsDemoState extends State with TickerProviderStateMixin { final GlobalKey _targetKey = GlobalKey(); // The radius of a hexagon tile in pixels. static const _kHexagonRadius = 16.0; // The margin between hexagons. static const _kHexagonMargin = 1.0; // The radius of the entire board in hexagons, not including the center. static const _kBoardRadius = 8; Board _board = Board( boardRadius: _kBoardRadius, hexagonRadius: _kHexagonRadius, hexagonMargin: _kHexagonMargin, ); final TransformationController _transformationController = TransformationController(); Animation? _animationReset; late AnimationController _controllerReset; Matrix4? _homeMatrix; // Handle reset to home transform animation. void _onAnimateReset() { _transformationController.value = _animationReset!.value; if (!_controllerReset.isAnimating) { _animationReset?.removeListener(_onAnimateReset); _animationReset = null; _controllerReset.reset(); } } // Initialize the reset to home transform animation. void _animateResetInitialize() { _controllerReset.reset(); _animationReset = Matrix4Tween( begin: _transformationController.value, end: _homeMatrix, ).animate(_controllerReset); _controllerReset.duration = const Duration(milliseconds: 400); _animationReset!.addListener(_onAnimateReset); _controllerReset.forward(); } // Stop a running reset to home transform animation. void _animateResetStop() { _controllerReset.stop(); _animationReset?.removeListener(_onAnimateReset); _animationReset = null; _controllerReset.reset(); } void _onScaleStart(ScaleStartDetails details) { // If the user tries to cause a transformation while the reset animation is // running, cancel the reset animation. if (_controllerReset.status == AnimationStatus.forward) { _animateResetStop(); } } void _onTapUp(TapUpDetails details) { final renderBox = _targetKey.currentContext!.findRenderObject() as RenderBox; final offset = details.globalPosition - renderBox.localToGlobal(Offset.zero); final scenePoint = _transformationController.toScene(offset); final boardPoint = _board.pointToBoardPoint(scenePoint); setState(() { _board = _board.copyWithSelected(boardPoint); }); } @override void initState() { super.initState(); _controllerReset = AnimationController( vsync: this, ); } @override Widget build(BuildContext context) { // The scene is drawn by a CustomPaint, but user interaction is handled by // the InteractiveViewer parent widget. return Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, appBar: AppBar( automaticallyImplyLeading: false, title: Text(GalleryLocalizations.of(context)!.demo2dTransformationsTitle), ), body: Container( color: backgroundColor, child: LayoutBuilder( builder: (context, constraints) { // Draw the scene as big as is available, but allow the user to // translate beyond that to a visibleSize that's a bit bigger. final viewportSize = Size( constraints.maxWidth, constraints.maxHeight, ); // Start the first render, start the scene centered in the viewport. if (_homeMatrix == null) { _homeMatrix = Matrix4.identity() ..translate( viewportSize.width / 2 - _board.size.width / 2, viewportSize.height / 2 - _board.size.height / 2, ); _transformationController.value = _homeMatrix!; } return ClipRect( child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTapUp: _onTapUp, child: InteractiveViewer( key: _targetKey, transformationController: _transformationController, boundaryMargin: EdgeInsets.symmetric( horizontal: viewportSize.width, vertical: viewportSize.height, ), minScale: 0.01, onInteractionStart: _onScaleStart, child: SizedBox.expand( child: CustomPaint( size: _board.size, painter: _BoardPainter( board: _board, ), ), ), ), ), ), ); }, ), ), persistentFooterButtons: [resetButton, editButton], ); } IconButton get resetButton { return IconButton( onPressed: () { setState(() { _animateResetInitialize(); }); }, tooltip: 'Reset', color: Theme.of(context).colorScheme.surface, icon: const Icon(Icons.replay), ); } IconButton get editButton { return IconButton( onPressed: () { if (_board.selected == null) { return; } showModalBottomSheet( context: context, builder: (context) { return Container( width: double.infinity, height: 150, padding: const EdgeInsets.all(12), child: EditBoardPoint( boardPoint: _board.selected!, onColorSelection: (color) { setState(() { _board = _board.copyWithBoardPointColor( _board.selected!, color); Navigator.pop(context); }); }, ), ); }); }, tooltip: 'Edit', color: Theme.of(context).colorScheme.surface, icon: const Icon(Icons.edit), ); } @override void dispose() { _controllerReset.dispose(); super.dispose(); } } // CustomPainter is what is passed to CustomPaint and actually draws the scene // when its `paint` method is called. class _BoardPainter extends CustomPainter { const _BoardPainter({required this.board}); final Board board; @override void paint(Canvas canvas, Size size) { void drawBoardPoint(BoardPoint? boardPoint) { final color = boardPoint!.color.withOpacity( board.selected == boardPoint ? 0.7 : 1, ); final vertices = board.getVerticesForBoardPoint(boardPoint, color); canvas.drawVertices(vertices, BlendMode.color, Paint()); } board.forEach(drawBoardPoint); } // We should repaint whenever the board changes, such as board.selected. @override bool shouldRepaint(_BoardPainter oldDelegate) { return oldDelegate.board != board; } } // END