mirror of https://github.com/flutter/samples.git
[Gallery] Add transformations demo (#291)
* Copy over transformations demo with changes Colors, size and buttons were changed * Remove <Widget>[] * Formatting * Expose gallery theme colors * Run grinder commandspull/295/head
parent
fcac28d65d
commit
7b0fdb2ddb
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,175 @@
|
|||||||
|
// 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 'dart:ui' show Vertices;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||||
|
import 'transformations_demo_board.dart';
|
||||||
|
import 'transformations_demo_edit_board_point.dart';
|
||||||
|
import 'transformations_demo_gesture_transformable.dart';
|
||||||
|
|
||||||
|
// BEGIN transformationsDemo#1
|
||||||
|
|
||||||
|
class TransformationsDemo extends StatefulWidget {
|
||||||
|
const TransformationsDemo({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TransformationsDemoState createState() => _TransformationsDemoState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TransformationsDemoState extends State<TransformationsDemo> {
|
||||||
|
// The radius of a hexagon tile in pixels.
|
||||||
|
static const _kHexagonRadius = 32.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 = 12;
|
||||||
|
|
||||||
|
bool _reset = false;
|
||||||
|
Board _board = Board(
|
||||||
|
boardRadius: _kBoardRadius,
|
||||||
|
hexagonRadius: _kHexagonRadius,
|
||||||
|
hexagonMargin: _kHexagonMargin,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final BoardPainter painter = BoardPainter(
|
||||||
|
board: _board,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The scene is drawn by a CustomPaint, but user interaction is handled by
|
||||||
|
// the GestureTransformable 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 Size size = Size(constraints.maxWidth, constraints.maxHeight);
|
||||||
|
final Size visibleSize = Size(size.width * 3, size.height * 2);
|
||||||
|
return GestureTransformable(
|
||||||
|
reset: _reset,
|
||||||
|
onResetEnd: () {
|
||||||
|
setState(() {
|
||||||
|
_reset = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: painter,
|
||||||
|
),
|
||||||
|
boundaryRect: Rect.fromLTWH(
|
||||||
|
-visibleSize.width / 2,
|
||||||
|
-visibleSize.height / 2,
|
||||||
|
visibleSize.width,
|
||||||
|
visibleSize.height,
|
||||||
|
),
|
||||||
|
// Center the board in the middle of the screen. It's drawn centered
|
||||||
|
// at the origin, which is the top left corner of the
|
||||||
|
// GestureTransformable.
|
||||||
|
initialTranslation: Offset(size.width / 2, size.height / 2),
|
||||||
|
onTapUp: _onTapUp,
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
persistentFooterButtons: [resetButton, editButton],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton get resetButton {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_reset = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip:
|
||||||
|
GalleryLocalizations.of(context).demo2dTransformationsResetTooltip,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
icon: const Icon(Icons.replay),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton get editButton {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_board.selected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showModalBottomSheet<Widget>(
|
||||||
|
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:
|
||||||
|
GalleryLocalizations.of(context).demo2dTransformationsEditTooltip,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUp(TapUpDetails details) {
|
||||||
|
final Offset scenePoint = details.globalPosition;
|
||||||
|
final BoardPoint boardPoint = _board.pointToBoardPoint(scenePoint);
|
||||||
|
setState(() {
|
||||||
|
_board = _board.copyWithSelected(boardPoint);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomPainter is what is passed to CustomPaint and actually draws the scene
|
||||||
|
// when its `paint` method is called.
|
||||||
|
class BoardPainter extends CustomPainter {
|
||||||
|
const BoardPainter({
|
||||||
|
this.board,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Board board;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
void drawBoardPoint(BoardPoint boardPoint) {
|
||||||
|
final Color color = boardPoint.color.withOpacity(
|
||||||
|
board.selected == boardPoint ? 0.7 : 1,
|
||||||
|
);
|
||||||
|
final Vertices 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
|
@ -0,0 +1,289 @@
|
|||||||
|
// 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 'dart:collection' show IterableMixin;
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui' show Vertices;
|
||||||
|
import 'package:flutter/material.dart' hide Gradient;
|
||||||
|
import 'package:vector_math/vector_math_64.dart' show Vector3;
|
||||||
|
|
||||||
|
// BEGIN transformationsDemo#2
|
||||||
|
|
||||||
|
// The entire state of the hex board and abstraction to get information about
|
||||||
|
// it. Iterable so that all BoardPoints on the board can be iterated over.
|
||||||
|
@immutable
|
||||||
|
class Board extends Object with IterableMixin<BoardPoint> {
|
||||||
|
Board({
|
||||||
|
@required this.boardRadius,
|
||||||
|
@required this.hexagonRadius,
|
||||||
|
@required this.hexagonMargin,
|
||||||
|
this.selected,
|
||||||
|
List<BoardPoint> boardPoints,
|
||||||
|
}) : assert(boardRadius > 0),
|
||||||
|
assert(hexagonRadius > 0),
|
||||||
|
assert(hexagonMargin >= 0) {
|
||||||
|
// Set up the positions for the center hexagon where the entire board is
|
||||||
|
// centered on the origin.
|
||||||
|
// Start point of hexagon (top vertex).
|
||||||
|
final Point<double> hexStart = Point<double>(0, -hexagonRadius);
|
||||||
|
final double hexagonRadiusPadded = hexagonRadius - hexagonMargin;
|
||||||
|
final double centerToFlat = sqrt(3) / 2 * hexagonRadiusPadded;
|
||||||
|
positionsForHexagonAtOrigin.addAll(<Offset>[
|
||||||
|
Offset(hexStart.x, hexStart.y),
|
||||||
|
Offset(hexStart.x + centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
|
||||||
|
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||||
|
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||||
|
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
|
||||||
|
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
|
||||||
|
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||||
|
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||||
|
Offset(hexStart.x - centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (boardPoints != null) {
|
||||||
|
_boardPoints.addAll(boardPoints);
|
||||||
|
} else {
|
||||||
|
// Generate boardPoints for a fresh board.
|
||||||
|
BoardPoint boardPoint = _getNextBoardPoint(null);
|
||||||
|
while (boardPoint != null) {
|
||||||
|
_boardPoints.add(boardPoint);
|
||||||
|
boardPoint = _getNextBoardPoint(boardPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int boardRadius; // Number of hexagons from center to edge.
|
||||||
|
final double hexagonRadius; // Pixel radius of a hexagon (center to vertex).
|
||||||
|
final double hexagonMargin; // Margin between hexagons.
|
||||||
|
final List<Offset> positionsForHexagonAtOrigin = <Offset>[];
|
||||||
|
final BoardPoint selected;
|
||||||
|
final List<BoardPoint> _boardPoints = <BoardPoint>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterator<BoardPoint> get iterator => _BoardIterator(_boardPoints);
|
||||||
|
|
||||||
|
// For a given q axial coordinate, get the range of possible r values
|
||||||
|
// See the definition of BoardPoint for more information about hex grids and
|
||||||
|
// axial coordinates.
|
||||||
|
_Range _getRRangeForQ(int q) {
|
||||||
|
int rStart;
|
||||||
|
int rEnd;
|
||||||
|
if (q <= 0) {
|
||||||
|
rStart = -boardRadius - q;
|
||||||
|
rEnd = boardRadius;
|
||||||
|
} else {
|
||||||
|
rEnd = boardRadius - q;
|
||||||
|
rStart = -boardRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _Range(rStart, rEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the BoardPoint that comes after the given BoardPoint. If given null,
|
||||||
|
// returns the origin BoardPoint. If given BoardPoint is the last, returns
|
||||||
|
// null.
|
||||||
|
BoardPoint _getNextBoardPoint(BoardPoint boardPoint) {
|
||||||
|
// If before the first element.
|
||||||
|
if (boardPoint == null) {
|
||||||
|
return BoardPoint(-boardRadius, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _Range rRange = _getRRangeForQ(boardPoint.q);
|
||||||
|
|
||||||
|
// If at or after the last element.
|
||||||
|
if (boardPoint.q >= boardRadius && boardPoint.r >= rRange.max) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If wrapping from one q to the next.
|
||||||
|
if (boardPoint.r >= rRange.max) {
|
||||||
|
return BoardPoint(boardPoint.q + 1, _getRRangeForQ(boardPoint.q + 1).min);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we're just incrementing r.
|
||||||
|
return BoardPoint(boardPoint.q, boardPoint.r + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the board point is actually on the board.
|
||||||
|
bool _validateBoardPoint(BoardPoint boardPoint) {
|
||||||
|
const BoardPoint center = BoardPoint(0, 0);
|
||||||
|
final int distanceFromCenter = getDistance(center, boardPoint);
|
||||||
|
return distanceFromCenter <= boardRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the distance between two BoardPoins.
|
||||||
|
static int getDistance(BoardPoint a, BoardPoint b) {
|
||||||
|
final Vector3 a3 = a.cubeCoordinates;
|
||||||
|
final Vector3 b3 = b.cubeCoordinates;
|
||||||
|
return ((a3.x - b3.x).abs() + (a3.y - b3.y).abs() + (a3.z - b3.z).abs()) ~/
|
||||||
|
2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the q,r BoardPoint for a point in the scene, where the origin is in
|
||||||
|
// the center of the board in both coordinate systems. If no BoardPoint at the
|
||||||
|
// location, return null.
|
||||||
|
BoardPoint pointToBoardPoint(Offset point) {
|
||||||
|
final BoardPoint boardPoint = BoardPoint(
|
||||||
|
((sqrt(3) / 3 * point.dx - 1 / 3 * point.dy) / hexagonRadius).round(),
|
||||||
|
((2 / 3 * point.dy) / hexagonRadius).round(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!_validateBoardPoint(boardPoint)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _boardPoints.firstWhere((boardPointI) {
|
||||||
|
return boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a scene point for the center of a hexagon given its q,r point.
|
||||||
|
Point<double> boardPointToPoint(BoardPoint boardPoint) {
|
||||||
|
return Point<double>(
|
||||||
|
sqrt(3) * hexagonRadius * boardPoint.q +
|
||||||
|
sqrt(3) / 2 * hexagonRadius * boardPoint.r,
|
||||||
|
1.5 * hexagonRadius * boardPoint.r,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Vertices that can be drawn to a Canvas for the given BoardPoint.
|
||||||
|
Vertices getVerticesForBoardPoint(BoardPoint boardPoint, Color color) {
|
||||||
|
final Point<double> centerOfHexZeroCenter = boardPointToPoint(boardPoint);
|
||||||
|
|
||||||
|
final List<Offset> positions = positionsForHexagonAtOrigin.map((offset) {
|
||||||
|
return offset.translate(centerOfHexZeroCenter.x, centerOfHexZeroCenter.y);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return Vertices(
|
||||||
|
VertexMode.triangleFan,
|
||||||
|
positions,
|
||||||
|
colors: List<Color>.filled(positions.length, color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new board with the given BoardPoint selected.
|
||||||
|
Board copyWithSelected(BoardPoint boardPoint) {
|
||||||
|
if (selected == boardPoint) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
final Board nextBoard = Board(
|
||||||
|
boardRadius: boardRadius,
|
||||||
|
hexagonRadius: hexagonRadius,
|
||||||
|
hexagonMargin: hexagonMargin,
|
||||||
|
selected: boardPoint,
|
||||||
|
boardPoints: _boardPoints,
|
||||||
|
);
|
||||||
|
return nextBoard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new board where boardPoint has the given color.
|
||||||
|
Board copyWithBoardPointColor(BoardPoint boardPoint, Color color) {
|
||||||
|
final BoardPoint nextBoardPoint = boardPoint.copyWithColor(color);
|
||||||
|
final int boardPointIndex = _boardPoints.indexWhere((boardPointI) =>
|
||||||
|
boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r);
|
||||||
|
|
||||||
|
if (elementAt(boardPointIndex) == boardPoint && boardPoint.color == color) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<BoardPoint> nextBoardPoints =
|
||||||
|
List<BoardPoint>.from(_boardPoints);
|
||||||
|
nextBoardPoints[boardPointIndex] = nextBoardPoint;
|
||||||
|
final BoardPoint selectedBoardPoint =
|
||||||
|
boardPoint == selected ? nextBoardPoint : selected;
|
||||||
|
return Board(
|
||||||
|
boardRadius: boardRadius,
|
||||||
|
hexagonRadius: hexagonRadius,
|
||||||
|
hexagonMargin: hexagonMargin,
|
||||||
|
selected: selectedBoardPoint,
|
||||||
|
boardPoints: nextBoardPoints,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BoardIterator extends Iterator<BoardPoint> {
|
||||||
|
_BoardIterator(this.boardPoints);
|
||||||
|
|
||||||
|
final List<BoardPoint> boardPoints;
|
||||||
|
int currentIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BoardPoint current;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool moveNext() {
|
||||||
|
if (currentIndex == null) {
|
||||||
|
currentIndex = 0;
|
||||||
|
} else {
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex >= boardPoints.length) {
|
||||||
|
current = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = boardPoints[currentIndex];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A range of q/r board coordinate values.
|
||||||
|
@immutable
|
||||||
|
class _Range {
|
||||||
|
const _Range(this.min, this.max)
|
||||||
|
: assert(min != null),
|
||||||
|
assert(max != null),
|
||||||
|
assert(min <= max);
|
||||||
|
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A location on the board in axial coordinates.
|
||||||
|
// Axial coordinates use two integers, q and r, to locate a hexagon on a grid.
|
||||||
|
// https://www.redblobgames.com/grids/hexagons/#coordinates-axial
|
||||||
|
@immutable
|
||||||
|
class BoardPoint {
|
||||||
|
const BoardPoint(
|
||||||
|
this.q,
|
||||||
|
this.r, {
|
||||||
|
this.color = const Color(0xFFCDCDCD),
|
||||||
|
});
|
||||||
|
|
||||||
|
final int q;
|
||||||
|
final int r;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BoardPoint($q, $r, $color)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only compares by location.
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is BoardPoint && other.q == q && other.r == r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(q, r);
|
||||||
|
|
||||||
|
BoardPoint copyWithColor(Color nextColor) =>
|
||||||
|
BoardPoint(q, r, color: nextColor);
|
||||||
|
|
||||||
|
// Convert from q,r axial coords to x,y,z cube coords.
|
||||||
|
Vector3 get cubeCoordinates {
|
||||||
|
return Vector3(
|
||||||
|
q.toDouble(),
|
||||||
|
r.toDouble(),
|
||||||
|
(-q - r).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// END
|
@ -0,0 +1,76 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
// A generic widget for a list of selectable colors.
|
||||||
|
@immutable
|
||||||
|
class ColorPicker extends StatelessWidget {
|
||||||
|
const ColorPicker({
|
||||||
|
@required this.colors,
|
||||||
|
@required this.selectedColor,
|
||||||
|
this.onColorSelection,
|
||||||
|
}) : assert(colors != null),
|
||||||
|
assert(selectedColor != null);
|
||||||
|
|
||||||
|
final Set<Color> colors;
|
||||||
|
final Color selectedColor;
|
||||||
|
final ValueChanged<Color> onColorSelection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: colors.map((color) {
|
||||||
|
return _ColorPickerSwatch(
|
||||||
|
color: color,
|
||||||
|
selected: color == selectedColor,
|
||||||
|
onTap: () {
|
||||||
|
if (onColorSelection != null) {
|
||||||
|
onColorSelection(color);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single selectable color widget in the ColorPicker.
|
||||||
|
@immutable
|
||||||
|
class _ColorPickerSwatch extends StatelessWidget {
|
||||||
|
const _ColorPickerSwatch({
|
||||||
|
@required this.color,
|
||||||
|
@required this.selected,
|
||||||
|
this.onTap,
|
||||||
|
}) : assert(color != null),
|
||||||
|
assert(selected != null);
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
final bool selected;
|
||||||
|
final Function onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
padding: const EdgeInsets.fromLTRB(2, 0, 2, 0),
|
||||||
|
child: RawMaterialButton(
|
||||||
|
fillColor: color,
|
||||||
|
onPressed: () {
|
||||||
|
if (onTap != null) {
|
||||||
|
onTap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: !selected
|
||||||
|
? null
|
||||||
|
: const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
// 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:gallery/themes/gallery_theme_data.dart';
|
||||||
|
import 'transformations_demo_board.dart';
|
||||||
|
import 'transformations_demo_color_picker.dart';
|
||||||
|
|
||||||
|
final backgroundColor = Color(0xFF272727);
|
||||||
|
|
||||||
|
// The panel for editing a board point.
|
||||||
|
@immutable
|
||||||
|
class EditBoardPoint extends StatelessWidget {
|
||||||
|
const EditBoardPoint({
|
||||||
|
Key key,
|
||||||
|
@required this.boardPoint,
|
||||||
|
this.onColorSelection,
|
||||||
|
}) : assert(boardPoint != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final BoardPoint boardPoint;
|
||||||
|
final ValueChanged<Color> onColorSelection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
print(GalleryThemeData.darkColorScheme);
|
||||||
|
|
||||||
|
final boardPointColors = <Color>{
|
||||||
|
Colors.white,
|
||||||
|
GalleryThemeData.darkColorScheme.primary,
|
||||||
|
GalleryThemeData.darkColorScheme.primaryVariant,
|
||||||
|
GalleryThemeData.darkColorScheme.secondary,
|
||||||
|
backgroundColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${boardPoint.q}, ${boardPoint.r}',
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
ColorPicker(
|
||||||
|
colors: boardPointColors,
|
||||||
|
selectedColor: boardPoint.color,
|
||||||
|
onColorSelection: onColorSelection,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,620 @@
|
|||||||
|
// 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:vector_math/vector_math_64.dart' show Vector3;
|
||||||
|
import 'transformations_demo_inertial_motion.dart';
|
||||||
|
|
||||||
|
// BEGIN transformationsDemo#3
|
||||||
|
|
||||||
|
// This widget allows 2D transform interactions on its child in relation to its
|
||||||
|
// parent. The user can transform the child by dragging to pan or pinching to
|
||||||
|
// zoom and rotate. All event callbacks for GestureDetector are supported, and
|
||||||
|
// the coordinates that are given are untransformed and in relation to the
|
||||||
|
// original position of the child.
|
||||||
|
@immutable
|
||||||
|
class GestureTransformable extends StatefulWidget {
|
||||||
|
const GestureTransformable({
|
||||||
|
Key key,
|
||||||
|
// The child to perform the transformations on.
|
||||||
|
@required this.child,
|
||||||
|
// The desired visible size of the widget and the area that is receptive to
|
||||||
|
// gestures. If a widget that's as big as possible is desired, then wrap
|
||||||
|
// this in a LayoutBuilder and pass
|
||||||
|
// `Size(constraints.maxWidth, constraints.maxHeight)`.
|
||||||
|
@required this.size,
|
||||||
|
// The scale will be clamped to between these values. A maxScale of null has
|
||||||
|
// no bounds. minScale must be greater than zero.
|
||||||
|
this.maxScale = 2.5,
|
||||||
|
this.minScale = 0.8,
|
||||||
|
// Transforms will be limited so that the viewport can not view beyond this
|
||||||
|
// Rect. The Rect does not rotate with the rest of the scene, so it is
|
||||||
|
// always aligned with the viewport. A null boundaryRect results in no
|
||||||
|
// limits to the distance that the viewport can be transformed to see.
|
||||||
|
this.boundaryRect,
|
||||||
|
// Initial values for the transform can be provided.
|
||||||
|
this.initialTranslation,
|
||||||
|
this.initialScale,
|
||||||
|
this.initialRotation,
|
||||||
|
// Any and all of the possible transformations can be disabled.
|
||||||
|
this.disableTranslation = false,
|
||||||
|
this.disableScale = false,
|
||||||
|
this.disableRotation = false,
|
||||||
|
// If set to true, this widget will animate back to its initial transform
|
||||||
|
// and call onResetEnd when done. When utilizing reset, onResetEnd should
|
||||||
|
// also be implemented, and it should set reset to false when called.
|
||||||
|
this.reset = false,
|
||||||
|
// Access to event callbacks from GestureDetector. Called with untransformed
|
||||||
|
// coordinates in an Offset.
|
||||||
|
this.onTapDown,
|
||||||
|
this.onTapUp,
|
||||||
|
this.onTap,
|
||||||
|
this.onTapCancel,
|
||||||
|
this.onDoubleTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.onLongPressUp,
|
||||||
|
this.onVerticalDragDown,
|
||||||
|
this.onVerticalDragStart,
|
||||||
|
this.onVerticalDragUpdate,
|
||||||
|
this.onVerticalDragEnd,
|
||||||
|
this.onVerticalDragCancel,
|
||||||
|
this.onHorizontalDragDown,
|
||||||
|
this.onHorizontalDragStart,
|
||||||
|
this.onHorizontalDragUpdate,
|
||||||
|
this.onHorizontalDragEnd,
|
||||||
|
this.onHorizontalDragCancel,
|
||||||
|
this.onPanDown,
|
||||||
|
this.onPanStart,
|
||||||
|
this.onPanUpdate,
|
||||||
|
this.onPanEnd,
|
||||||
|
this.onPanCancel,
|
||||||
|
this.onResetEnd,
|
||||||
|
this.onScaleStart,
|
||||||
|
this.onScaleUpdate,
|
||||||
|
this.onScaleEnd,
|
||||||
|
}) : assert(child != null),
|
||||||
|
assert(size != null),
|
||||||
|
assert(minScale != null),
|
||||||
|
assert(minScale > 0),
|
||||||
|
assert(disableTranslation != null),
|
||||||
|
assert(disableScale != null),
|
||||||
|
assert(disableRotation != null),
|
||||||
|
assert(reset != null),
|
||||||
|
assert(
|
||||||
|
!reset || onResetEnd != null,
|
||||||
|
'Must implement onResetEnd to use reset.',
|
||||||
|
),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final Size size;
|
||||||
|
final bool reset;
|
||||||
|
final GestureTapDownCallback onTapDown;
|
||||||
|
final GestureTapUpCallback onTapUp;
|
||||||
|
final GestureTapCallback onTap;
|
||||||
|
final GestureTapCancelCallback onTapCancel;
|
||||||
|
final GestureTapCallback onDoubleTap;
|
||||||
|
final GestureLongPressCallback onLongPress;
|
||||||
|
final GestureLongPressUpCallback onLongPressUp;
|
||||||
|
final GestureDragDownCallback onVerticalDragDown;
|
||||||
|
final GestureDragStartCallback onVerticalDragStart;
|
||||||
|
final GestureDragUpdateCallback onVerticalDragUpdate;
|
||||||
|
final GestureDragEndCallback onVerticalDragEnd;
|
||||||
|
final GestureDragCancelCallback onVerticalDragCancel;
|
||||||
|
final GestureDragDownCallback onHorizontalDragDown;
|
||||||
|
final GestureDragStartCallback onHorizontalDragStart;
|
||||||
|
final GestureDragUpdateCallback onHorizontalDragUpdate;
|
||||||
|
final GestureDragEndCallback onHorizontalDragEnd;
|
||||||
|
final GestureDragCancelCallback onHorizontalDragCancel;
|
||||||
|
final GestureDragDownCallback onPanDown;
|
||||||
|
final GestureDragStartCallback onPanStart;
|
||||||
|
final GestureDragUpdateCallback onPanUpdate;
|
||||||
|
final GestureDragEndCallback onPanEnd;
|
||||||
|
final GestureDragCancelCallback onPanCancel;
|
||||||
|
final VoidCallback onResetEnd;
|
||||||
|
final GestureScaleStartCallback onScaleStart;
|
||||||
|
final GestureScaleUpdateCallback onScaleUpdate;
|
||||||
|
final GestureScaleEndCallback onScaleEnd;
|
||||||
|
final double maxScale;
|
||||||
|
final double minScale;
|
||||||
|
final Rect boundaryRect;
|
||||||
|
final bool disableTranslation;
|
||||||
|
final bool disableScale;
|
||||||
|
final bool disableRotation;
|
||||||
|
final Offset initialTranslation;
|
||||||
|
final double initialScale;
|
||||||
|
final double initialRotation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_GestureTransformableState createState() => _GestureTransformableState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single user event can only represent one of these gestures. The user can't
|
||||||
|
// do multiple at the same time, which results in more precise transformations.
|
||||||
|
enum _GestureType {
|
||||||
|
translate,
|
||||||
|
scale,
|
||||||
|
rotate,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is public only for access from a unit test.
|
||||||
|
class _GestureTransformableState extends State<GestureTransformable>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
Animation<Offset> _animation;
|
||||||
|
AnimationController _controller;
|
||||||
|
Animation<Matrix4> _animationReset;
|
||||||
|
AnimationController _controllerReset;
|
||||||
|
// The translation that will be applied to the scene (not viewport).
|
||||||
|
// A positive x offset moves the scene right, viewport left.
|
||||||
|
// A positive y offset moves the scene down, viewport up.
|
||||||
|
Offset _translateFromScene; // Point where a single translation began.
|
||||||
|
double _scaleStart; // Scale value at start of scaling gesture.
|
||||||
|
double _rotationStart = 0; // Rotation at start of rotation gesture.
|
||||||
|
Rect _boundaryRect;
|
||||||
|
Matrix4 _transform = Matrix4.identity();
|
||||||
|
double _currentRotation = 0;
|
||||||
|
_GestureType gestureType;
|
||||||
|
|
||||||
|
// The transformation matrix that gives the initial home position.
|
||||||
|
Matrix4 get _initialTransform {
|
||||||
|
Matrix4 matrix = Matrix4.identity();
|
||||||
|
if (widget.initialTranslation != null) {
|
||||||
|
matrix = matrixTranslate(matrix, widget.initialTranslation);
|
||||||
|
}
|
||||||
|
if (widget.initialScale != null) {
|
||||||
|
matrix = matrixScale(matrix, widget.initialScale);
|
||||||
|
}
|
||||||
|
if (widget.initialRotation != null) {
|
||||||
|
matrix = matrixRotate(matrix, widget.initialRotation, Offset.zero);
|
||||||
|
}
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the scene point at the given viewport point.
|
||||||
|
static Offset fromViewport(Offset viewportPoint, Matrix4 transform) {
|
||||||
|
// On viewportPoint, perform the inverse transformation of the scene to get
|
||||||
|
// where the point would be in the scene before the transformation.
|
||||||
|
final Matrix4 inverseMatrix = Matrix4.inverted(transform);
|
||||||
|
final Vector3 untransformed = inverseMatrix.transform3(Vector3(
|
||||||
|
viewportPoint.dx,
|
||||||
|
viewportPoint.dy,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
return Offset(untransformed.x, untransformed.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the offset of the current widget from the global screen coordinates.
|
||||||
|
// TODO(justinmc): Protect against calling this during first build.
|
||||||
|
static Offset getOffset(BuildContext context) {
|
||||||
|
final RenderBox renderObject = context.findRenderObject() as RenderBox;
|
||||||
|
return renderObject.localToGlobal(Offset.zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_boundaryRect = widget.boundaryRect ?? Offset.zero & widget.size;
|
||||||
|
_transform = _initialTransform;
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_controllerReset = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
if (widget.reset) {
|
||||||
|
_animateResetInitialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(GestureTransformable oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.reset && !oldWidget.reset && _animationReset == null) {
|
||||||
|
_animateResetInitialize();
|
||||||
|
} else if (!widget.reset && oldWidget.reset && _animationReset != null) {
|
||||||
|
_animateResetStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// A GestureDetector allows the detection of panning and zooming gestures on
|
||||||
|
// its child, which is the CustomPaint.
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque, // Necessary when translating off screen
|
||||||
|
onTapDown: widget.onTapDown == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onTapDown(TapDownDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onTapUp: widget.onTapUp == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onTapUp(TapUpDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onTap: widget.onTap,
|
||||||
|
onTapCancel: widget.onTapCancel,
|
||||||
|
onDoubleTap: widget.onDoubleTap,
|
||||||
|
onLongPress: widget.onLongPress,
|
||||||
|
onLongPressUp: widget.onLongPressUp,
|
||||||
|
onVerticalDragDown: widget.onVerticalDragDown == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onVerticalDragDown(DragDownDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onVerticalDragStart: widget.onVerticalDragStart == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onVerticalDragStart(DragStartDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onVerticalDragUpdate: widget.onVerticalDragUpdate == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onVerticalDragUpdate(DragUpdateDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onVerticalDragEnd: widget.onVerticalDragEnd,
|
||||||
|
onVerticalDragCancel: widget.onVerticalDragCancel,
|
||||||
|
onHorizontalDragDown: widget.onHorizontalDragDown == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onHorizontalDragDown(DragDownDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onHorizontalDragStart: widget.onHorizontalDragStart == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onHorizontalDragStart(DragStartDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onHorizontalDragUpdate: widget.onHorizontalDragUpdate == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onHorizontalDragUpdate(DragUpdateDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onHorizontalDragEnd: widget.onHorizontalDragEnd,
|
||||||
|
onHorizontalDragCancel: widget.onHorizontalDragCancel,
|
||||||
|
onPanDown: widget.onPanDown == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onPanDown(DragDownDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onPanStart: widget.onPanStart == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onPanStart(DragStartDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onPanUpdate: widget.onPanUpdate == null
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
widget.onPanUpdate(DragUpdateDetails(
|
||||||
|
globalPosition: fromViewport(
|
||||||
|
details.globalPosition - getOffset(context), _transform),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onPanEnd: widget.onPanEnd,
|
||||||
|
onPanCancel: widget.onPanCancel,
|
||||||
|
onScaleEnd: _onScaleEnd,
|
||||||
|
onScaleStart: _onScaleStart,
|
||||||
|
onScaleUpdate: _onScaleUpdate,
|
||||||
|
child: ClipRect(
|
||||||
|
// The scene is panned/zoomed/rotated using this Transform widget.
|
||||||
|
child: Transform(
|
||||||
|
transform: _transform,
|
||||||
|
child: Container(
|
||||||
|
child: widget.child,
|
||||||
|
height: widget.size.height,
|
||||||
|
width: widget.size.width,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new matrix representing the given matrix after applying the given
|
||||||
|
// translation.
|
||||||
|
Matrix4 matrixTranslate(Matrix4 matrix, Offset translation) {
|
||||||
|
if (widget.disableTranslation || translation == Offset.zero) {
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp translation so the viewport remains inside _boundaryRect.
|
||||||
|
final double scale = _transform.getMaxScaleOnAxis();
|
||||||
|
final Size scaledSize = widget.size / scale;
|
||||||
|
final Rect viewportBoundaries = Rect.fromLTRB(
|
||||||
|
_boundaryRect.left,
|
||||||
|
_boundaryRect.top,
|
||||||
|
_boundaryRect.right - scaledSize.width,
|
||||||
|
_boundaryRect.bottom - scaledSize.height,
|
||||||
|
);
|
||||||
|
// Translation is reversed (a positive translation moves the scene to the
|
||||||
|
// right, viewport to the left).
|
||||||
|
final Rect translationBoundaries = Rect.fromLTRB(
|
||||||
|
-scale * viewportBoundaries.right,
|
||||||
|
-scale * viewportBoundaries.bottom,
|
||||||
|
-scale * viewportBoundaries.left,
|
||||||
|
-scale * viewportBoundaries.top,
|
||||||
|
);
|
||||||
|
final Matrix4 nextMatrix = matrix.clone()
|
||||||
|
..translate(
|
||||||
|
translation.dx,
|
||||||
|
translation.dy,
|
||||||
|
);
|
||||||
|
final Vector3 nextTranslationVector = nextMatrix.getTranslation();
|
||||||
|
final Offset nextTranslation = Offset(
|
||||||
|
nextTranslationVector.x,
|
||||||
|
nextTranslationVector.y,
|
||||||
|
);
|
||||||
|
final bool inBoundaries = translationBoundaries.contains(
|
||||||
|
Offset(nextTranslation.dx, nextTranslation.dy),
|
||||||
|
);
|
||||||
|
if (!inBoundaries) {
|
||||||
|
// TODO(justinmc): Instead of canceling translation when it goes out of
|
||||||
|
// bounds, stop translation at boundary.
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new matrix representing the given matrix after applying the given
|
||||||
|
// scale transform.
|
||||||
|
Matrix4 matrixScale(Matrix4 matrix, double scale) {
|
||||||
|
if (widget.disableScale || scale == 1) {
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
assert(scale != 0);
|
||||||
|
|
||||||
|
// Don't allow a scale that moves the viewport outside of _boundaryRect.
|
||||||
|
final Offset tl = fromViewport(const Offset(0, 0), _transform);
|
||||||
|
final Offset tr = fromViewport(Offset(widget.size.width, 0), _transform);
|
||||||
|
final Offset bl = fromViewport(Offset(0, widget.size.height), _transform);
|
||||||
|
final Offset br = fromViewport(
|
||||||
|
Offset(widget.size.width, widget.size.height),
|
||||||
|
_transform,
|
||||||
|
);
|
||||||
|
if (!_boundaryRect.contains(tl) ||
|
||||||
|
!_boundaryRect.contains(tr) ||
|
||||||
|
!_boundaryRect.contains(bl) ||
|
||||||
|
!_boundaryRect.contains(br)) {
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow a scale that results in an overall scale beyond min/max
|
||||||
|
// scale.
|
||||||
|
final double currentScale = _transform.getMaxScaleOnAxis();
|
||||||
|
final double totalScale = currentScale * scale;
|
||||||
|
final double clampedTotalScale = totalScale.clamp(
|
||||||
|
widget.minScale,
|
||||||
|
widget.maxScale,
|
||||||
|
) as double;
|
||||||
|
final double clampedScale = clampedTotalScale / currentScale;
|
||||||
|
return matrix..scale(clampedScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new matrix representing the given matrix after applying the given
|
||||||
|
// rotation transform.
|
||||||
|
// Rotating the scene cannot cause the viewport to view beyond _boundaryRect.
|
||||||
|
Matrix4 matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) {
|
||||||
|
if (widget.disableRotation || rotation == 0) {
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
final Offset focalPointScene = fromViewport(focalPoint, matrix);
|
||||||
|
return matrix
|
||||||
|
..translate(focalPointScene.dx, focalPointScene.dy)
|
||||||
|
..rotateZ(-rotation)
|
||||||
|
..translate(-focalPointScene.dx, -focalPointScene.dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the start of a gesture of _GestureType.
|
||||||
|
void _onScaleStart(ScaleStartDetails details) {
|
||||||
|
if (widget.onScaleStart != null) {
|
||||||
|
widget.onScaleStart(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_controller.isAnimating) {
|
||||||
|
_controller.stop();
|
||||||
|
_controller.reset();
|
||||||
|
_animation?.removeListener(_onAnimate);
|
||||||
|
_animation = null;
|
||||||
|
}
|
||||||
|
if (_controllerReset.isAnimating) {
|
||||||
|
_animateResetStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
gestureType = null;
|
||||||
|
setState(() {
|
||||||
|
_scaleStart = _transform.getMaxScaleOnAxis();
|
||||||
|
_translateFromScene = fromViewport(details.focalPoint, _transform);
|
||||||
|
_rotationStart = _currentRotation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle an update to an ongoing gesture of _GestureType.
|
||||||
|
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||||
|
double scale = _transform.getMaxScaleOnAxis();
|
||||||
|
if (widget.onScaleUpdate != null) {
|
||||||
|
widget.onScaleUpdate(ScaleUpdateDetails(
|
||||||
|
focalPoint: fromViewport(details.focalPoint, _transform),
|
||||||
|
scale: details.scale,
|
||||||
|
rotation: details.rotation,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
final Offset focalPointScene = fromViewport(
|
||||||
|
details.focalPoint,
|
||||||
|
_transform,
|
||||||
|
);
|
||||||
|
if (gestureType == null) {
|
||||||
|
// Decide which type of gesture this is by comparing the amount of scale
|
||||||
|
// and rotation in the gesture, if any. Scale starts at 1 and rotation
|
||||||
|
// starts at 0. Translate will have 0 scale and 0 rotation because it uses
|
||||||
|
// only one finger.
|
||||||
|
if ((details.scale - 1).abs() > details.rotation.abs()) {
|
||||||
|
gestureType = _GestureType.scale;
|
||||||
|
} else if (details.rotation != 0) {
|
||||||
|
gestureType = _GestureType.rotate;
|
||||||
|
} else {
|
||||||
|
gestureType = _GestureType.translate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
if (gestureType == _GestureType.scale && _scaleStart != null) {
|
||||||
|
// details.scale gives us the amount to change the scale as of the
|
||||||
|
// start of this gesture, so calculate the amount to scale as of the
|
||||||
|
// previous call to _onScaleUpdate.
|
||||||
|
final double desiredScale = _scaleStart * details.scale;
|
||||||
|
final double scaleChange = desiredScale / scale;
|
||||||
|
_transform = matrixScale(_transform, scaleChange);
|
||||||
|
scale = _transform.getMaxScaleOnAxis();
|
||||||
|
|
||||||
|
// While scaling, translate such that the user's two fingers stay on the
|
||||||
|
// same places in the scene. That means that the focal point of the
|
||||||
|
// scale should be on the same place in the scene before and after the
|
||||||
|
// scale.
|
||||||
|
final Offset focalPointSceneNext = fromViewport(
|
||||||
|
details.focalPoint,
|
||||||
|
_transform,
|
||||||
|
);
|
||||||
|
_transform =
|
||||||
|
matrixTranslate(_transform, focalPointSceneNext - focalPointScene);
|
||||||
|
} else if (gestureType == _GestureType.rotate && details.rotation != 0) {
|
||||||
|
final double desiredRotation = _rotationStart + details.rotation;
|
||||||
|
_transform = matrixRotate(
|
||||||
|
_transform, _currentRotation - desiredRotation, details.focalPoint);
|
||||||
|
_currentRotation = desiredRotation;
|
||||||
|
} else if (_translateFromScene != null && details.scale == 1) {
|
||||||
|
// Translate so that the same point in the scene is underneath the
|
||||||
|
// focal point before and after the movement.
|
||||||
|
final Offset translationChange = focalPointScene - _translateFromScene;
|
||||||
|
_transform = matrixTranslate(_transform, translationChange);
|
||||||
|
_translateFromScene = fromViewport(details.focalPoint, _transform);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the end of a gesture of _GestureType.
|
||||||
|
void _onScaleEnd(ScaleEndDetails details) {
|
||||||
|
if (widget.onScaleEnd != null) {
|
||||||
|
widget.onScaleEnd(details);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_scaleStart = null;
|
||||||
|
_rotationStart = null;
|
||||||
|
_translateFromScene = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
_animation?.removeListener(_onAnimate);
|
||||||
|
_controller.reset();
|
||||||
|
|
||||||
|
// If the scale ended with velocity, animate inertial movement
|
||||||
|
final double velocityTotal = details.velocity.pixelsPerSecond.dx.abs() +
|
||||||
|
details.velocity.pixelsPerSecond.dy.abs();
|
||||||
|
if (velocityTotal == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Vector3 translationVector = _transform.getTranslation();
|
||||||
|
final Offset translation = Offset(translationVector.x, translationVector.y);
|
||||||
|
final InertialMotion inertialMotion =
|
||||||
|
InertialMotion(details.velocity, translation);
|
||||||
|
_animation = Tween<Offset>(
|
||||||
|
begin: translation,
|
||||||
|
end: inertialMotion.finalPosition,
|
||||||
|
).animate(_controller);
|
||||||
|
_controller.duration =
|
||||||
|
Duration(milliseconds: inertialMotion.duration.toInt());
|
||||||
|
_animation.addListener(_onAnimate);
|
||||||
|
_controller.fling();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle inertia drag animation.
|
||||||
|
void _onAnimate() {
|
||||||
|
setState(() {
|
||||||
|
// Translate _transform such that the resulting translation is
|
||||||
|
// _animation.value.
|
||||||
|
final Vector3 translationVector = _transform.getTranslation();
|
||||||
|
final Offset translation =
|
||||||
|
Offset(translationVector.x, translationVector.y);
|
||||||
|
final Offset translationScene = fromViewport(translation, _transform);
|
||||||
|
final Offset animationScene = fromViewport(_animation.value, _transform);
|
||||||
|
final Offset translationChangeScene = animationScene - translationScene;
|
||||||
|
_transform = matrixTranslate(_transform, translationChangeScene);
|
||||||
|
});
|
||||||
|
if (!_controller.isAnimating) {
|
||||||
|
_animation?.removeListener(_onAnimate);
|
||||||
|
_animation = null;
|
||||||
|
_controller.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reset to home transform animation.
|
||||||
|
void _onAnimateReset() {
|
||||||
|
setState(() {
|
||||||
|
_transform = _animationReset.value;
|
||||||
|
});
|
||||||
|
if (!_controllerReset.isAnimating) {
|
||||||
|
_animationReset?.removeListener(_onAnimateReset);
|
||||||
|
_animationReset = null;
|
||||||
|
_controllerReset.reset();
|
||||||
|
widget.onResetEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the reset to home transform animation.
|
||||||
|
void _animateResetInitialize() {
|
||||||
|
_controllerReset.reset();
|
||||||
|
_animationReset = Matrix4Tween(
|
||||||
|
begin: _transform,
|
||||||
|
end: _initialTransform,
|
||||||
|
).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();
|
||||||
|
widget.onResetEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
_controllerReset.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// END
|
@ -0,0 +1,72 @@
|
|||||||
|
// 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 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:vector_math/vector_math.dart' show Vector2;
|
||||||
|
|
||||||
|
// Provides calculations for an object moving with inertia and friction using
|
||||||
|
// the equation of motion from physics.
|
||||||
|
// https://en.wikipedia.org/wiki/Equations_of_motion#Constant_translational_acceleration_in_a_straight_line
|
||||||
|
// TODO(justinmc): Can this be replaced with friction_simulation.dart?
|
||||||
|
@immutable
|
||||||
|
class InertialMotion {
|
||||||
|
const InertialMotion(this._initialVelocity, this._initialPosition);
|
||||||
|
|
||||||
|
static const double _kFrictionalAcceleration = 0.01; // How quickly to stop
|
||||||
|
final Velocity _initialVelocity;
|
||||||
|
final Offset _initialPosition;
|
||||||
|
|
||||||
|
// The position when the motion stops.
|
||||||
|
Offset get finalPosition {
|
||||||
|
return _getPositionAt(Duration(milliseconds: duration.toInt()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The total time that the animation takes start to stop in milliseconds.
|
||||||
|
double get duration {
|
||||||
|
return (_initialVelocity.pixelsPerSecond.dx / 1000 / _acceleration.x).abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The acceleration opposing the initial velocity in x and y components.
|
||||||
|
Vector2 get _acceleration {
|
||||||
|
// TODO(justinmc): Find actual velocity instead of summing?
|
||||||
|
final velocityTotal = _initialVelocity.pixelsPerSecond.dx.abs() +
|
||||||
|
_initialVelocity.pixelsPerSecond.dy.abs();
|
||||||
|
final vRatioX = _initialVelocity.pixelsPerSecond.dx / velocityTotal;
|
||||||
|
final vRatioY = _initialVelocity.pixelsPerSecond.dy / velocityTotal;
|
||||||
|
return Vector2(
|
||||||
|
_kFrictionalAcceleration * vRatioX,
|
||||||
|
_kFrictionalAcceleration * vRatioY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The position at a given time.
|
||||||
|
Offset _getPositionAt(Duration time) {
|
||||||
|
final xf = _getPosition(
|
||||||
|
r0: _initialPosition.dx,
|
||||||
|
v0: _initialVelocity.pixelsPerSecond.dx / 1000,
|
||||||
|
t: time.inMilliseconds,
|
||||||
|
a: _acceleration.x,
|
||||||
|
);
|
||||||
|
final yf = _getPosition(
|
||||||
|
r0: _initialPosition.dy,
|
||||||
|
v0: _initialVelocity.pixelsPerSecond.dy / 1000,
|
||||||
|
t: time.inMilliseconds,
|
||||||
|
a: _acceleration.y,
|
||||||
|
);
|
||||||
|
return Offset(xf, yf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solve the equation of motion to find the position at a given point in time
|
||||||
|
// in one dimension.
|
||||||
|
double _getPosition({double r0, double v0, int t, double a}) {
|
||||||
|
// Stop movement when it would otherwise reverse direction.
|
||||||
|
final stopTime = (v0 / a).abs();
|
||||||
|
if (t > stopTime) {
|
||||||
|
t = stopTime.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0 + v0 * t + 0.5 * a * pow(t, 2);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue