Related
So I have a third-party library in my Flutter app which sends me messages.
A message can for example be represented in the following json format
{
objects: [
{
'name': 'Name of obj1',
'x1': 107,
'y1': 1012,
'x2': 117,
'y2': 974
},
...
]
}
The x and y propertiess are screen coordinates (express in pixels).
Suppose now that I want to draw a Text (with the name property of an object in the message) that animates from (x1, y1) to (x2, y2) based on the data from the messages.
I looked at AnimatedPositioned which refered me to SlideTransition
If the size is intended to remain the same, with only the position changing over time, then consider SlideTransition instead. SlideTransition only triggers a repaint each frame of the animation, whereas AnimatedPositioned will trigger a relayout as well.
Now the example provided in the flutter doc of SlideTransition works with Offset going from Offset.zero to Offset(1.5, 0.0). But what exactly are these units expressed in? And how can I provide my screen coordinates to the Flutter Offset units?
I deliberatly gave a bit of the background to note that I cannot change anything about the format that these messages come in. I simply need to convert the screen coordinates to an offset, relative to the root of the application, not to its parent ofcourse.
double leftPosition = data[0]['x1'];
double topPosition = data[0]['y1'];
Call a future.delayed setting left and top position to the next value like
Future.delayed(duration: Duration (seconds:1), (){
leftPosition = data[0]['x2'];
topPosition = data[0]['y2'];
setState((){});
});
Then
Stack(
children:[
AnimatedPositioned(
duration : Duration(seconds : 1),
left : leftPosition,
top: topPosition,
)
]
),
This should take values in pixels. Please excuse if there are any mistakes. Wrote it from my phone.
if you "need FULL control over my widgets" i would recommend CustomMultiChildLayout which not only lets you know your child widgets size during the layout phase (so you can align them within some Offset) but also supports "animated" layout without rebuilding the children - in "normal" Stack widget you would need to wrap it with LayoutBuilder (to get the whole layout size) and AnimatedBuilder (for motion animation) and also you would need to rebuild your children on every animation frame
here you have two versions: the first one uses Overlay and has a very simple logic of child positioning, the second one uses RenderBox and the child positioning is more complex
the version that uses Overlay:
class AnimatedLabels extends StatefulWidget {
#override
State<AnimatedLabels> createState() => _AnimatedLabelsState();
}
class _AnimatedLabelsState extends State<AnimatedLabels> with TickerProviderStateMixin {
late AnimationController controller;
late OverlayEntry entry;
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
entry = OverlayEntry(
builder: (ctx) {
final colors = [
Colors.orange, Colors.green, Colors.lime,
];
final data = [
{'name': 'orange label', 'x1': 150.0, 'y1': 400.0, 'x2': 50.0, 'y2': 100.0},
{'name': 'green label', 'x1': 10.0, 'y1': 56.0, 'x2': 65.0, 'y2': 125.0},
{'name': 'lime label', 'x1': 200.0, 'y1': 56.0, 'x2': 80.0, 'y2': 150.0},
];
return CustomMultiChildLayout(
delegate: LabelDelegate(controller, data),
children: List.generate(3, (i) => LayoutId(
id: i,
child: Card(
margin: EdgeInsets.zero,
elevation: 3,
color: colors[i],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(data[i]['name'] as String),
),
),
)),
);
},
);
SchedulerBinding.instance.addPostFrameCallback(insertOverlay);
}
insertOverlay(Duration d) {
print('insertOverlay $entry');
Overlay.of(context)!.insert(entry);
}
#override
Widget build(BuildContext context) {
print('build');
return Center(
child: ElevatedButton(
child: const Text('press me to start animation'),
onPressed: () => controller.value < 0.5? controller.forward() : controller.reverse()
),
);
}
}
class LabelDelegate extends MultiChildLayoutDelegate {
final AnimationController controller;
final List<Map> offsets;
LabelDelegate(this.controller, List<Map> data) :
offsets = [for (final d in data) {
'from': Offset(d['x1'] as double, d['y1'] as double),
'to': Offset(d['x2'] as double, d['y2'] as double),
}],
super(relayout: controller);
#override
void performLayout(ui.Size size) {
final looseBoxConstraints = BoxConstraints.loose(size);
// final curve = controller.status == AnimationStatus.forward? Curves.elasticOut : Curves.elasticIn;
final curve = controller.status == AnimationStatus.forward? Curves.easeOutBack : Curves.easeInBack;
final t = curve.transform(controller.value);
int id = 0;
for (final offsetMap in offsets) {
// print('$id: $offsetMap');
final size = layoutChild(id, looseBoxConstraints);
// the most important line of this code:
positionChild(id, Offset.lerp(offsetMap['from'], offsetMap['to'], t)!);
id++;
}
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
}
the version that uses RenderBox:
class AnimatedLabels2 extends StatefulWidget {
#override
State<AnimatedLabels2> createState() => _AnimatedLabels2State();
}
class _AnimatedLabels2State extends State<AnimatedLabels2> with TickerProviderStateMixin {
late AnimationController controller;
final key = GlobalKey();
final globalOffset = ValueNotifier<Offset?>(null);
final colors = [
Colors.orange, Colors.green, Colors.lime,
];
final data = [
{'name': 'orange label', 'x1': 150.0, 'y1': 400.0, 'x2': 50.0, 'y2': 100.0},
{'name': 'green label', 'x1': 10.0, 'y1': 56.0, 'x2': 65.0, 'y2': 125.0},
{'name': 'lime label', 'x1': 200.0, 'y1': 56.0, 'x2': 80.0, 'y2': 150.0},
];
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
SchedulerBinding.instance.addPostFrameCallback(setGlobalOffset);
}
setGlobalOffset(Duration d) {
final renderBox = key.currentContext!.findRenderObject() as RenderBox;
globalOffset.value = renderBox.localToGlobal(Offset.zero);
print('globalOffset: ${globalOffset.value}');
}
#override
Widget build(BuildContext context) {
print('build');
return Stack(
key: key,
children: [
Center(
child: ElevatedButton(
child: const Text('press me to start animation'),
onPressed: () => controller.value < 0.5? controller.forward() : controller.reverse()
),
),
CustomMultiChildLayout(
delegate: LabelDelegate2(controller, data, globalOffset),
children: List.generate(3, (i) => LayoutId(
id: i,
child: Card(
margin: EdgeInsets.zero,
elevation: 3,
color: colors[i],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(data[i]['name'] as String),
),
),
)),
),
],
);
}
}
class LabelDelegate2 extends MultiChildLayoutDelegate {
final AnimationController controller;
final List<Map> offsets;
final ValueNotifier<Offset?> globalOffset;
LabelDelegate2(this.controller, List<Map> data, this.globalOffset) :
offsets = [for (final d in data) {
'from': Offset(d['x1'] as double, d['y1'] as double),
'to': Offset(d['x2'] as double, d['y2'] as double),
}],
super(relayout: Listenable.merge([controller, globalOffset]));
#override
void performLayout(ui.Size size) {
// print(globalOffset);
final looseBoxConstraints = BoxConstraints.loose(size);
// final curve = controller.status == AnimationStatus.forward? Curves.elasticOut : Curves.elasticIn;
final curve = controller.status == AnimationStatus.forward? Curves.easeOutBack : Curves.easeInBack;
final t = curve.transform(controller.value);
int id = 0;
for (final offsetMap in offsets) {
// print('$id: $offsetMap');
final size = layoutChild(id, looseBoxConstraints);
// the most important line of this code:
positionChild(id, Offset.lerp(offsetMap['from'], offsetMap['to'], t)! - (globalOffset.value ?? Offset.zero));
id++;
}
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
}
EDIT added a modified version of RenderBox solution showing how to implement the case when the change of location happens when the animation is still running: note that not only the whole animation is continuous but also the animated object behaves as if it was a real object with some mass (so it does not change the final direction at once) - it was done by simple use of SpringSimulation
class AnimatedLabels3 extends StatefulWidget {
#override
State<AnimatedLabels3> createState() => _AnimatedLabels3State();
}
class _AnimatedLabels3State extends State<AnimatedLabels3> with TickerProviderStateMixin {
late AnimationController controllerX, controllerY;
late LabelDelegate3 delegate;
final key = GlobalKey();
final globalOffset = ValueNotifier<Offset?>(null);
#override
void initState() {
super.initState();
controllerX = AnimationController.unbounded(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
controllerY = AnimationController.unbounded(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
delegate = LabelDelegate3(controllerX, controllerY, globalOffset);
SchedulerBinding.instance.addPostFrameCallback(setGlobalOffset);
}
setGlobalOffset(Duration d) {
final renderBox = key.currentContext!.findRenderObject() as RenderBox;
globalOffset.value = renderBox.localToGlobal(Offset.zero);
print('globalOffset: ${globalOffset.value}');
}
#override
Widget build(BuildContext context) {
print('build');
return Stack(
children: [
const Center(child: Text('tap anywhere to start animation\n\nalso try to tup again while animation is still in progress')),
GestureDetector(
onPanDown: (d) {
delegate.simulateNewTargetPosition(d.globalPosition);
},
),
CustomSingleChildLayout(
key: key,
delegate: delegate,
child: SizedBox.square(
dimension: 64,
child: Container(
width: 64,
height: 64,
decoration: const ShapeDecoration(
color: Colors.deepOrange,
shape: CircleBorder(),
shadows: [BoxShadow(blurRadius: 4, spreadRadius: 1, offset: Offset(3, 3))],
),
),
),
),
],
);
}
}
final springDescription = SpringDescription.withDampingRatio(mass: 8, stiffness: 100);
class LabelDelegate3 extends SingleChildLayoutDelegate {
final AnimationController controllerX;
final AnimationController controllerY;
final ValueNotifier<Offset?> globalOffset;
Offset current = Offset.zero;
Simulation
sx = SpringSimulation(springDescription, 0, 0, 0),
sy = SpringSimulation(springDescription, 0, 0, 0);
LabelDelegate3(this.controllerX, this.controllerY, this.globalOffset) :
super(relayout: Listenable.merge([controllerX, controllerX, globalOffset]));
#override
Offset getPositionForChild(Size size, Size childSize) {
current = Offset(controllerX.value, controllerY.value);
// the most important line of this code:
return current - (globalOffset.value ?? Offset.zero) - childSize.center(Offset.zero);
}
void simulateNewTargetPosition(ui.Offset position) {
// timeDilation = 5;
sx = SpringSimulation(springDescription, current.dx, position.dx, 2 * controllerX.velocity);
sy = SpringSimulation(springDescription, current.dy, position.dy, 2 * controllerY.velocity);
controllerX.animateWith(sx);
controllerY.animateWith(sy);
}
#override
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) => true;
}
I'm working on a drawing app. I want to enable both drawing on the canvas and zooming the drawings. So far, I tried achieving it by wrapping GestureDetector in InteractiveViewer and using AbsorbPointer to turn the zoom mode on and off. See the minimum demo code below.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MaterialApp(
home: new IssueExamplePage(),
debugShowCheckedModeBanner: false,
));
class IssueExamplePage extends StatefulWidget {
#override
_IssueExamplePageState createState() => _IssueExamplePageState();
}
class _IssueExamplePageState extends State<IssueExamplePage> {
bool drawingBlocked = false;
List<Offset> points = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
color: Colors.grey,
padding: EdgeInsets.all(5),
child: Container(
color: Colors.white,
child: InteractiveViewer(
child: AbsorbPointer(
absorbing: drawingBlocked,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: (details) {
RenderBox renderBox = context.findRenderObject();
Offset cursorLocation = renderBox.globalToLocal(details.localPosition);
setState(() {
points = List.of(points)..add(cursorLocation);
});
},
onPanEnd: (details) {
setState(() {
points = List.of(points)..add(null);
});
},
child: CustomPaint(
painter: MyPainter(points),
size: Size.infinite
),
),
),
),
),
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
child: Icon(drawingBlocked? CupertinoIcons.hand_raised_fill : CupertinoIcons.hand_raised),
onPressed: () {
setState(() {
drawingBlocked = !drawingBlocked;
});
}
),
SizedBox(
height: 10
),
FloatingActionButton(
child: Icon(Icons.clear),
onPressed: () {
setState(() {
points = [];
});
}
),
SizedBox(
height: 20
),
],
),
);
}
}
class MyPainter extends CustomPainter {
MyPainter(this.points);
List<Offset> points;
Paint paintBrush = Paint()
..color = Colors.blue
..strokeWidth = 5
..strokeJoin = StrokeJoin.round
..strokeCap = StrokeCap.round;
#override
void paint(Canvas canvas, Size size) {
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paintBrush);
}
}
}
#override
bool shouldRepaint(MyPainter oldDelegate) {
return points != oldDelegate.points;
}
}
However, with this realization OnPanUpdate is working with a delay (see Flutter onPanStart invokes late when the widget is wrapped inside InteractiveViewer). Is there any other way to achieve the expected result (both zooming and drawing) or is it possible to fix the OnPanUpdate delay?
UPDATE: I found some sort of solution. You can use Listener instead of GestureDetector (it has substitues for the most of the parameters: onPanStart -> onPointerDown, onPanUpdate -> onPointerMove, etc.). But it seems like there's no fix for GestureDetector.
Depending on how my drawer is built, some child components receive top padding, messing up childs behavior, including this DropdownButton 'dropwdown button list' to misalign.
Drawer(
child: Column(
children: <Widget>[
Container(color: Colors.amber,
child: DropdownButtonHideUnderline(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
value: 'Company 1',
onChanged: (String newValue) {
// TODO change company
},
items: <String>['Company 1', 'Company 2', 'Company 3', 'Company 4'].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
),
),
Expanded(
child: ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.home),
title: Text(AppLocalizations.of(context).homeDrawerHomeTitle),
onTap: () async => GetIt.I.get<NavigationService>().pushReplacementNamed(AppRouter.root),
),
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context).homeDrawerLogoutTitle),
onTap: () async => GetIt.I.get<AuthenticationService>().signOutUser(),
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: Column(
children: <Widget>[
Divider(),
StreamBuilder<User>(
stream: GetIt.I.get<AuthenticationService>().getCurrentUserStream(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListTile(
leading: Icon(Icons.login),
title: Text(AppLocalizations.of(context).homeDrawerProfileTitle),
onTap: () {
GetIt.I.get<NavigationService>().navigateTo(AppRouter.profile);
},
);
}
return ListTile(
leading: Icon(Icons.login),
title: Text(AppLocalizations.of(context).homeDrawerLoginTitle),
onTap: () {
GetIt.I.get<NavigationService>().navigateTo(AppRouter.login);
},
);
},
),
ListTile(
leading: Icon(Icons.settings),
title: Text(AppLocalizations.of(context).homeDrawerSettingsTitle),
onTap: () {
_scaffoldKey.currentState.removeCurrentSnackBar();
// TODO change route to settings
GetIt.I.get<NavigationService>().navigateTo(AppRouter.root);
},
),
SizedBox(
height: 5.0,
)
],
),
)
],
),
),
Image pointing the 'forehead' that magically appears in the 'dropwdown button list' that should not be there:
Image pointing the padding required or the 'dropwdown button list' does not open correctly over the DropDownButton:
My main questions are:
Why the Container around the DropdownButton widgets needs a '56' top
padding or the 'dropwdown button list' misaligns when clicked/opened?
What is 'pushing down' the 'dropwdown button list' out of alignment?
Why the list is also getting more top padding out of nowhere?
Create Custom Class For DropdownButton and write this.
import 'dart:math' as math;
import 'package:flutter/material.dart';
const Duration _kDropdownMenuDuration = Duration(milliseconds: 300);
const double _kMenuItemHeight = 48.0;
const double _kDenseButtonHeight = 24.0;
const EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
const EdgeInsetsGeometry _kAlignedButtonPadding =
EdgeInsetsDirectional.only(start: 16.0, end: 4.0);
const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero;
const EdgeInsetsGeometry _kUnalignedMenuMargin =
EdgeInsetsDirectional.only(start: 16.0, end: 24.0);
class _DropdownMenuPainter extends CustomPainter {
_DropdownMenuPainter({
this.color,
this.elevation,
this.selectedIndex,
this.resize,
}) : _painter = new BoxDecoration(
// If you add an image here, you must provide a real
// configuration in the paint() function and you must provide some sort
// of onChanged callback here.
color: color,
borderRadius: new BorderRadius.circular(2.0),
boxShadow: kElevationToShadow[elevation])
.createBoxPainter(),
super(repaint: resize);
final Color color;
final int elevation;
final int selectedIndex;
final Animation<double> resize;
final BoxPainter _painter;
#override
void paint(Canvas canvas, Size size) {
final double selectedItemOffset =
selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
final Tween<double> top = new Tween<double>(
begin: selectedItemOffset.clamp(0.0, size.height - _kMenuItemHeight),
end: 0.0,
);
final Tween<double> bottom = new Tween<double>(
begin:
(top.begin + _kMenuItemHeight).clamp(_kMenuItemHeight, size.height),
end: size.height,
);
final Rect rect = new Rect.fromLTRB(
0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
_painter.paint(
canvas, rect.topLeft, new ImageConfiguration(size: rect.size));
}
#override
bool shouldRepaint(_DropdownMenuPainter oldPainter) {
return oldPainter.color != color ||
oldPainter.elevation != elevation ||
oldPainter.selectedIndex != selectedIndex ||
oldPainter.resize != resize;
}
}
// Do not use the platform-specific default scroll configuration.
// Dropdown menus should never overscroll or display an overscroll indicator.
class _DropdownScrollBehavior extends ScrollBehavior {
const _DropdownScrollBehavior();
#override
TargetPlatform getPlatform(BuildContext context) =>
Theme.of(context).platform;
#override
Widget buildViewportChrome(
BuildContext context, Widget child, AxisDirection axisDirection) =>
child;
#override
ScrollPhysics getScrollPhysics(BuildContext context) =>
const ClampingScrollPhysics();
}
class _DropdownMenu<T> extends StatefulWidget {
const _DropdownMenu({
Key key,
this.padding,
this.route,
}) : super(key: key);
final _DropdownRoute<T> route;
final EdgeInsets padding;
#override
_DropdownMenuState<T> createState() => new _DropdownMenuState<T>();
}
class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
CurvedAnimation _fadeOpacity;
CurvedAnimation _resize;
#override
void initState() {
super.initState();
// We need to hold these animations as state because of their curve
// direction. When the route's animation reverses, if we were to recreate
// the CurvedAnimation objects in build, we'd lose
// CurvedAnimation._curveDirection.
_fadeOpacity = new CurvedAnimation(
parent: widget.route.animation,
curve: const Interval(0.0, 0.25),
reverseCurve: const Interval(0.75, 1.0),
);
_resize = new CurvedAnimation(
parent: widget.route.animation,
curve: const Interval(0.25, 0.5),
reverseCurve: const Threshold(0.0),
);
}
#override
Widget build(BuildContext context) {
// The menu is shown in three stages (unit timing in brackets):
// [0s - 0.25s] - Fade in a rect-sized menu container with the selected item.
// [0.25s - 0.5s] - Grow the otherwise empty menu container from the center
// until it's big enough for as many items as we're going to show.
// [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
//
// When the menu is dismissed we just fade the entire thing out
// in the first 0.25s.
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final _DropdownRoute<T> route = widget.route;
final double unit = 0.5 / (route.items.length + 1.5);
final List<Widget> children = <Widget>[];
for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) {
CurvedAnimation opacity;
if (itemIndex == route.selectedIndex) {
opacity = new CurvedAnimation(
parent: route.animation, curve: const Threshold(0.0));
} else {
final double start = (0.5 + (itemIndex + 1) * unit).clamp(0.0, 1.0);
final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
opacity = new CurvedAnimation(
parent: route.animation, curve: new Interval(start, end));
}
children.add(new FadeTransition(
opacity: opacity,
child: new InkWell(
child: new Container(
padding: widget.padding,
child: route.items[itemIndex],
),
onTap: () => Navigator.pop(
context,
new _DropdownRouteResult<T>(route.items[itemIndex].value),
),
),
));
}
return new FadeTransition(
opacity: _fadeOpacity,
child: new CustomPaint(
painter: new _DropdownMenuPainter(
color: Theme.of(context).canvasColor,
elevation: route.elevation,
selectedIndex: route.selectedIndex,
resize: _resize,
),
child: new Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: localizations.popupMenuLabel,
child: new Material(
type: MaterialType.transparency,
textStyle: route.style,
child: new ScrollConfiguration(
behavior: const _DropdownScrollBehavior(),
child: new Scrollbar(
child: new ListView(
controller: widget.route.scrollController,
padding: kMaterialListPadding,
itemExtent: _kMenuItemHeight,
shrinkWrap: true,
children: children,
),
),
),
),
),
),
);
}
}
class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
_DropdownMenuRouteLayout({
#required this.buttonRect,
#required this.menuTop,
#required this.menuHeight,
#required this.textDirection,
});
final Rect buttonRect;
final double menuTop;
final double menuHeight;
final TextDirection textDirection;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// The maximum height of a simple menu should be one or more rows less than
// the view height. This ensures a tappable area outside of the simple menu
// with which to dismiss the menu.
// -- https://material.google.com/components/menus.html#menus-simple-menus
final double maxHeight =
math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
// The width of a menu should be at most the view width. This ensures that
// the menu does not extend past the left and right edges of the screen.
final double width = math.min(constraints.maxWidth, buttonRect.width);
return new BoxConstraints(
minWidth: width,
maxWidth: width,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
assert(() {
final Rect container = Offset.zero & size; // this gives the zero offset
if (container.intersect(buttonRect) == buttonRect) {
// If the button was entirely on-screen, then verify
// that the menu is also on-screen.
// If the button was a bit off-screen, then, oh well.
assert(menuTop >= 0.0);
assert(menuTop + menuHeight <= size.height);
}
return true;
}());
assert(textDirection != null);
double left;
switch (textDirection) {
case TextDirection.rtl:
left = buttonRect.right.clamp(0.0, size.width) - childSize.width;
break;
case TextDirection.ltr:
left = buttonRect.left.clamp(0.0, size.width - childSize.width);
break;
}
return new Offset(left, menuTop);
}
#override
bool shouldRelayout(_DropdownMenuRouteLayout<T> oldDelegate) {
return buttonRect != oldDelegate.buttonRect ||
menuTop != oldDelegate.menuTop ||
menuHeight != oldDelegate.menuHeight ||
textDirection != oldDelegate.textDirection;
}
}
class _DropdownRouteResult<T> {
const _DropdownRouteResult(this.result);
final T result;
#override
bool operator ==(dynamic other) {
if (other is! _DropdownRouteResult<T>) return false;
final _DropdownRouteResult<T> typedOther = other;
return result == typedOther.result;
}
#override
int get hashCode => result.hashCode;
}
class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
_DropdownRoute({
this.items,
this.padding,
this.buttonRect,
this.selectedIndex,
this.elevation = 8,
this.theme,
#required this.style,
this.barrierLabel,
}) : assert(style != null);
final List<DropdownMenuItem<T>> items;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final int selectedIndex;
final int elevation;
final ThemeData theme;
final TextStyle style;
ScrollController scrollController;
#override
Duration get transitionDuration => _kDropdownMenuDuration;
#override
bool get barrierDismissible => true;
#override
Color get barrierColor => null;
#override
final String barrierLabel;
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
assert(debugCheckHasDirectionality(context));
final double screenHeight = MediaQuery.of(context).size.height;
final double maxMenuHeight = screenHeight - 2.0 * _kMenuItemHeight;
final double preferredMenuHeight =
(items.length * _kMenuItemHeight) + kMaterialListPadding.vertical;
final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
final double buttonTop = buttonRect.top;
final double selectedItemOffset =
selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
double menuTop = (buttonTop - selectedItemOffset) -
(_kMenuItemHeight - buttonRect.height) / 2.0;
const double topPreferredLimit = _kMenuItemHeight;
if (menuTop < topPreferredLimit)
menuTop = math.min(buttonTop, topPreferredLimit);
double bottom = menuTop + menuHeight;
final double bottomPreferredLimit = screenHeight - _kMenuItemHeight;
if (bottom > bottomPreferredLimit) {
bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
menuTop = bottom - menuHeight;
}
if (scrollController == null) {
double scrollOffset = 0.0;
if (preferredMenuHeight > maxMenuHeight)
scrollOffset = selectedItemOffset - (buttonTop - menuTop);
scrollController =
new ScrollController(initialScrollOffset: scrollOffset);
}
final TextDirection textDirection = Directionality.of(context);
Widget menu = new _DropdownMenu<T>(
route: this,
padding: padding.resolve(textDirection),
);
if (theme != null) menu = new Theme(data: theme, child: menu);
return new MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
child: new Builder(
builder: (BuildContext context) {
return new CustomSingleChildLayout(
delegate: new _DropdownMenuRouteLayout<T>(
buttonRect: buttonRect,
menuTop: menuTop,
menuHeight: menuHeight,
textDirection: textDirection,
),
child: menu,
);
},
),
);
}
void _dismiss() {
navigator?.removeRoute(this);
}
}
class CustomDropdownButton<T> extends StatefulWidget {
/// Creates a dropdown button.
///
/// The [items] must have distinct values and if [value] isn't null it must be among them.
///
/// The [elevation] and [iconSize] arguments must not be null (they both have
/// defaults, so do not need to be specified).
CustomDropdownButton({
Key key,
#required this.items,
this.value,
this.hint,
#required this.onChanged,
this.elevation = 8,
this.style,
this.iconSize = 24.0,
this.isDense = false,
}) : assert(items != null),
assert(value == null ||
items
.where((DropdownMenuItem<T> item) => item.value == value)
.length ==
1),
super(key: key);
/// The list of possible items to select among.
final List<DropdownMenuItem<T>> items;
/// The currently selected item, or null if no item has been selected. If
/// value is null then the menu is popped up as if the first item was
/// selected.
final T value;
/// Displayed if [value] is null.
final Widget hint;
/// Called when the user selects an item.
final ValueChanged<T> onChanged;
/// The z-coordinate at which to place the menu when open.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 8, the appropriate elevation for dropdown buttons.
final int elevation;
/// The text style to use for text in the dropdown button and the dropdown
/// menu that appears when you tap the button.
///
/// Defaults to the [TextTheme.subhead] value of the current
/// [ThemeData.textTheme] of the current [Theme].
final TextStyle style;
/// The size to use for the drop-down button's down arrow icon button.
///
/// Defaults to 24.0.
final double iconSize;
/// Reduce the button's height.
///
/// By default this button's height is the same as its menu items' heights.
/// If isDense is true, the button's height is reduced by about half. This
/// can be useful when the button is embedded in a container that adds
/// its own decorations, like [InputDecorator].
final bool isDense;
#override
_DropdownButtonState<T> createState() => new _DropdownButtonState<T>();
}
class _DropdownButtonState<T> extends State<CustomDropdownButton<T>>
with WidgetsBindingObserver {
int _selectedIndex;
_DropdownRoute<T> _dropdownRoute;
#override
void initState() {
super.initState();
// _updateSelectedIndex();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_removeDropdownRoute();
super.dispose();
}
// Typically called because the device's orientation has changed.
// Defined by WidgetsBindingObserver
#override
void didChangeMetrics() {
_removeDropdownRoute();
}
void _removeDropdownRoute() {
_dropdownRoute?._dismiss();
_dropdownRoute = null;
}
#override
void didUpdateWidget(CustomDropdownButton<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_updateSelectedIndex();
}
void _updateSelectedIndex() {
assert(widget.value == null ||
widget.items
.where((DropdownMenuItem<T> item) => item.value == widget.value)
.length ==
1);
_selectedIndex = null;
for (int itemIndex = 0; itemIndex < widget.items.length; itemIndex++) {
if (widget.items[itemIndex].value == widget.value) {
_selectedIndex = itemIndex;
return;
}
}
}
TextStyle get _textStyle =>
widget.style ?? Theme.of(context).textTheme.subhead;
void _handleTap() {
final RenderBox itemBox = context.findRenderObject();
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
final TextDirection textDirection = Directionality.of(context);
final EdgeInsetsGeometry menuMargin =
ButtonTheme.of(context).alignedDropdown
? _kAlignedMenuMargin
: _kUnalignedMenuMargin;
assert(_dropdownRoute == null);
_dropdownRoute = new _DropdownRoute<T>(
items: widget.items,
buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
padding: _kMenuItemPadding.resolve(textDirection),
selectedIndex: -1,
elevation: widget.elevation,
theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
);
Navigator.push(context, _dropdownRoute)
.then<void>((_DropdownRouteResult<T> newValue) {
_dropdownRoute = null;
if (!mounted || newValue == null) return;
if (widget.onChanged != null) widget.onChanged(newValue.result);
});
}
// When isDense is true, reduce the height of this button from _kMenuItemHeight to
// _kDenseButtonHeight, but don't make it smaller than the text that it contains.
// Similarly, we don't reduce the height of the button so much that its icon
// would be clipped.
double get _denseButtonHeight {
return math.max(
_textStyle.fontSize, math.max(widget.iconSize, _kDenseButtonHeight));
}
#override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
// The width of the button and the menu are defined by the widest
// item and the width of the hint.
final List<Widget> items = new List<Widget>.from(widget.items);
int hintIndex;
if (widget.hint != null) {
hintIndex = items.length;
items.add(new DefaultTextStyle(
style: _textStyle.copyWith(color: Theme.of(context).hintColor),
child: new IgnorePointer(
child: widget.hint,
ignoringSemantics: false,
),
));
}
final EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown
? _kAlignedButtonPadding
: _kUnalignedButtonPadding;
Widget result = new DefaultTextStyle(
style: _textStyle,
child: new Container(
padding: padding.resolve(Directionality.of(context)),
height: widget.isDense ? _denseButtonHeight : null,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// If value is null (then _selectedIndex is null) then we display
// the hint or nothing at all.
Expanded(
child: new IndexedStack(
index: _selectedIndex ?? hintIndex,
alignment: AlignmentDirectional.centerStart,
children: items,
),
),
new Icon(Icons.arrow_drop_down,
size: widget.iconSize,
// These colors are not defined in the Material Design spec.
color: Theme.of(context).brightness == Brightness.light
? Colors.grey.shade700
: Colors.white70),
],
),
),
);
if (!DropdownButtonHideUnderline.at(context)) {
final double bottom = widget.isDense ? 0.0 : 8.0;
result = new Stack(
children: <Widget>[
result,
new Positioned(
left: 0.0,
right: 0.0,
bottom: bottom,
child: new Container(
height: 1.0,
decoration: const BoxDecoration(
border: Border(
bottom:
BorderSide(color: Color(0xFFBDBDBD), width: 0.0))),
),
),
],
);
}
return new Semantics(
button: true,
child: new GestureDetector(
onTap: _handleTap, behavior: HitTestBehavior.opaque, child: result),
);
}
}
this gives the zero offset of your drop-down button.
you can use like this
CustomDropdownButton(
value: Company,
items: dropdownItems,
onChanged: onChange,
),
you can customize it as you want like padding and margin and icon as well
I have a Problem with the CustomPainter Widget. I want to draw a PieChart which works fine, then I added a Variable which draws the Chart to until it reached this angle. Now I want to animate it, I used the Future.delayed function and in there with setState I wanted to update the variable but that doesn't work unfortunately.
I am developing for the web. Thanks for helping!
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stats/data/listLanguages.dart';
import 'painter/pieChartPainter.dart';
class Chart extends StatefulWidget {
ListLanguages listLanguages;
Chart({ListLanguages listLanguages}) {
if (listLanguages == null) {
listLanguages = new ListLanguages();
}
this.listLanguages = listLanguages;
}
#override
_ChartState createState() => _ChartState();
}
class _ChartState extends State<Chart> {
#override
Widget build(BuildContext context) {
List angles = widget.listLanguages.calcCounts();
int angle = 0;
Future.delayed(new Duration(seconds: 2), (){
setState(() {
angle = 360;
print("test");
});
});
return Column(
children: [
Spacer(flex: 2),
Row(
children: [
Spacer(),
CustomPaint(
size: Size.square(400),
painter: PieChartPainter(
angles: angles,
colors: new List()
..add(Colors.green)
..add(Colors.blue)
..add(Colors.brown)
..add(Colors.pink)
..add(Colors.orange)
..add(Colors.grey.shade700),
angle: angle,
),
),
Spacer(flex: 10),
],
),
Spacer(flex: 3),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as vm;
class PieChartPainter extends CustomPainter {
List angles, colors;
int angle;
PieChartPainter(
{#required List angles, #required List colors, int angle: 360}) {
this.angles = angles;
this.colors = colors;
this.angle = angle;
}
#override
void paint(Canvas canvas, Size size) {
Paint p = new Paint();
double start = -90;
double tmp = 0;
for (int i = 0; i < angles.length; i++) {
if (i < 5) {
p.color = colors[i];
} else {
p.color = colors[5];
}
if (tmp + angles[i] < angle) {
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(angles[i]), true, p);
start = start + angles[i];
tmp = tmp + angles[i];
} else {
double x = angle - tmp;
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(x), true, p);
return;
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
this is the complete code I have to create the Pie Chart
You can copy paste run full code below
In your case, to work with Future.delayed, you can move logic from build to initState and use addPostFrameCallback
working demo change angle in 2, 4, 6 seconds and angle is 150, 250, 360
code snippet
class _ChartState extends State<Chart> {
int angle = 0;
List angles;
#override
void initState() {
angles = widget.listLanguages.calcCounts();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 2), () {
setState(() {
angle = 150;
});
});
Future.delayed(Duration(seconds: 4), () {
setState(() {
angle = 250;
});
});
Future.delayed(Duration(seconds: 6), () {
setState(() {
angle = 360;
});
});
});
working demo
full code
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as vm;
class ListLanguages {
List calcCounts() {
return [10.0, 20.0, 100.0, 150.0, 250.0, 300.0];
}
}
class Chart extends StatefulWidget {
ListLanguages listLanguages;
Chart({ListLanguages listLanguages}) {
if (listLanguages == null) {
listLanguages = ListLanguages();
}
this.listLanguages = listLanguages;
}
#override
_ChartState createState() => _ChartState();
}
class _ChartState extends State<Chart> {
int angle = 0;
List angles;
#override
void initState() {
angles = widget.listLanguages.calcCounts();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 2), () {
print("delay");
setState(() {
angle = 150;
print("test");
});
});
Future.delayed(Duration(seconds: 4), () {
print("delay");
setState(() {
angle = 250;
print("test");
});
});
Future.delayed(Duration(seconds: 6), () {
print("delay");
setState(() {
angle = 360;
print("test");
});
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Spacer(flex: 2),
Row(
children: [
Spacer(),
CustomPaint(
size: Size.square(400),
painter: PieChartPainter(
angles: angles,
colors: List()
..add(Colors.green)
..add(Colors.blue)
..add(Colors.brown)
..add(Colors.pink)
..add(Colors.orange)
..add(Colors.grey.shade700),
angle: angle,
),
),
Spacer(flex: 10),
],
),
Spacer(flex: 3),
],
);
}
}
class PieChartPainter extends CustomPainter {
List angles, colors;
int angle;
PieChartPainter(
{#required List angles, #required List colors, int angle: 360}) {
this.angles = angles;
this.colors = colors;
this.angle = angle;
}
#override
void paint(Canvas canvas, Size size) {
Paint p = Paint();
double start = -90;
double tmp = 0;
for (int i = 0; i < angles.length; i++) {
if (i < 5) {
p.color = colors[i];
} else {
p.color = colors[5];
}
if (tmp + angles[i] < angle) {
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(angles[i]), true, p);
start = start + angles[i];
tmp = tmp + angles[i];
} else {
double x = angle - tmp;
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(x), true, p);
return;
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Chart(
listLanguages: ListLanguages(),
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
I can not use your code so that I can run it (since it's only a small part) but what you need is:
Define an animation and animation controller in your state
Surround your CustomPainter with an "AnimatedBuilder" which will use this animation and will pass the value between 0 to 360 to your CustomPainter in 2 seconds.
Below is an example with comments (which you will have to take parts from and put in to your widget).
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
// NOTE: You need to add "SingleTickerProviderStateMixin" for animation to work
class _TestState extends State<Test> with SingleTickerProviderStateMixin {
Animation _animation; // Stores animation
AnimationController _controller; // Stores controller
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
); // Create a 2 second duration controller
_animation = IntTween(begin: 0, end: 360)
.animate(_controller); // Create the animation using controller with a tween from 0 to 360
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.forward(); // Start the animation when widget is displayed
});
}
#override
void dispose() {
_controller.dispose(); // Don't forget to dispose your controller
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder( // AnimatedBuilder using the animation
animation: _animation,
builder: (context, _){
return CustomPaint(
size: Size.square(400),
painter: PieChartPainter(
angles: angles,
colors: new List()
..add(Colors.green)
..add(Colors.blue)
..add(Colors.brown)
..add(Colors.pink)
..add(Colors.orange)
..add(Colors.grey.shade700),
angle: _animation.value, // Pass _animation.value (0 to 360) as your angle
),
);
},
);
}
}
I want to add a new option in the text selection toolbar, an extra option apart of the classics cut, copy, paste, selectAll.
enter image description here
I used SelectableText but its toolbarOptions just let active/desactive the classic options, not create a new one. So I'm trying using EditableText and creating my own text_selection.dart copying the material text_selection class
I supose there is something wrong when I call my text_selection class because the toolbar doesn't show and there is not any error message.
enter image description here
Here is my widget that use my text_selection class
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'my_text_selection_controls.dart';
class PageContent extends StatefulWidget {
final String page = 'a text just for testing';
PageContent();
#override
_PageContentState createState() => _PageContentState();
}
class _PageContentState extends State<PageContent> {
var textController = new TextEditingController();
FocusNode _textfieldFocusNode;
#override
void initState(){
super.initState();
_textfieldFocusNode = FocusNode();
textController.text = widget.page;
}
#override
void dispose() {
_textfieldFocusNode.dispose();
textController.dispose();
super.dispose();
}
void screenTapped(){
print('calling a callback');
}
#override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: GestureDetector(
onTap: () => screenTapped(),
child: Container(
alignment: Alignment.center,
child:
EditableText(
focusNode: _textfieldFocusNode,
controller: textController,
backgroundCursorColor: Colors.lightGreen,
selectionColor: Colors.blue,
style: TextStyle(color: Colors.black, fontSize: 17),
cursorColor: Colors.blue,
textInputAction: TextInputAction.newline,
maxLines: null,
enableInteractiveSelection: true,
selectionControls: mymaterialTextSelectionControls, //USING MY TEXT SELECTION CLASS
),
color: Color(0xfffdf5e6),
)
),
),
);
}
}
And here is my_text_selection class. Is just a copy of the material class.
// Copyright 2016 The Chromium Authors. 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' as math;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
const double _kHandleSize = 22.0;
// Minimal padding from all edges of the selection toolbar to all edges of the
// viewport.
const double _kToolbarScreenPadding = 8.0;
const double _kToolbarHeight = 44.0;
// Padding when positioning toolbar below selection.
const double _kToolbarContentDistanceBelow = 16.0;
const double _kToolbarContentDistance = 8.0;
/// Manages a copy/paste text selection toolbar.
class _TextSelectionToolbar extends StatelessWidget {
const _TextSelectionToolbar({
Key key,
this.handleCut,
this.handleCopy,
this.handlePaste,
this.handleSelectAll,
}) : super(key: key);
final VoidCallback handleCut;
final VoidCallback handleCopy;
final VoidCallback handlePaste;
final VoidCallback handleSelectAll;
#override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final List<Widget> items = <Widget>[
if (handleCut != null) FlatButton(child: Text(localizations.cutButtonLabel), onPressed: handleCut),
if (handleCopy != null) FlatButton(child: Text(localizations.copyButtonLabel), onPressed: handleCopy),
if (handlePaste != null) FlatButton(child: Text(localizations.pasteButtonLabel), onPressed: handlePaste),
if (handleSelectAll != null) FlatButton(child: Text(localizations.selectAllButtonLabel), onPressed: handleSelectAll),
];
// If there is no option available, build an empty widget.
if (items.isEmpty) {
return Container(width: 0.0, height: 0.0);
}
return Material(
elevation: 1.0,
child: Container(
height: _kToolbarHeight,
child: Row(mainAxisSize: MainAxisSize.min, children: items),
),
);
}
}
/// Centers the toolbar around the given position, ensuring that it remains on
/// screen.
class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
_TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position);
/// The size of the screen at the time that the toolbar was last laid out.
final Size screenSize;
/// Size and position of the editing region at the time the toolbar was last
/// laid out, in global coordinates.
final Rect globalEditableRegion;
/// Anchor position of the toolbar, relative to the top left of the
/// [globalEditableRegion].
final Offset position;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.loosen();
}
#override
Offset getPositionForChild(Size size, Size childSize) {
final Offset globalPosition = globalEditableRegion.topLeft + position;
double x = globalPosition.dx - childSize.width / 2.0;
double y = globalPosition.dy - childSize.height;
if (x < _kToolbarScreenPadding)
x = _kToolbarScreenPadding;
else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding)
x = screenSize.width - childSize.width - _kToolbarScreenPadding;
if (y < _kToolbarScreenPadding)
y = _kToolbarScreenPadding;
else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding)
y = screenSize.height - childSize.height - _kToolbarScreenPadding;
return Offset(x, y);
}
#override
bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) {
return position != oldDelegate.position;
}
}
/// Draws a single text selection handle which points up and to the left.
class _TextSelectionHandlePainter extends CustomPainter {
_TextSelectionHandlePainter({ this.color });
final Color color;
#override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = color;
final double radius = size.width/2.0;
canvas.drawCircle(Offset(radius, radius), radius, paint);
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, radius, radius), paint);
}
#override
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
return color != oldPainter.color;
}
}
class _MaterialTextSelectionControls extends TextSelectionControls {
/// Returns the size of the Material handle.
#override
Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize);
/// Builder for material-style copy/paste text selection toolbar.
#override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset position,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
// The toolbar should appear below the TextField
// when there is not enough space above the TextField to show it.
final TextSelectionPoint startTextSelectionPoint = endpoints[0];
final double toolbarHeightNeeded = MediaQuery.of(context).padding.top
+ _kToolbarScreenPadding
+ _kToolbarHeight
+ _kToolbarContentDistance;
final double availableHeight = globalEditableRegion.top + endpoints.first.point.dy - textLineHeight;
final bool fitsAbove = toolbarHeightNeeded <= availableHeight;
final double y = fitsAbove
? startTextSelectionPoint.point.dy - _kToolbarContentDistance - textLineHeight
: startTextSelectionPoint.point.dy + _kToolbarHeight + _kToolbarContentDistanceBelow;
final Offset preciseMidpoint = Offset(position.dx, y);
return ConstrainedBox(
constraints: BoxConstraints.tight(globalEditableRegion.size),
child: CustomSingleChildLayout(
delegate: _TextSelectionToolbarLayout(
MediaQuery.of(context).size,
globalEditableRegion,
preciseMidpoint,
),
child: _TextSelectionToolbar(
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
),
),
);
}
/// Builder for material-style text selection handles.
#override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) {
final Widget handle = SizedBox(
width: _kHandleSize,
height: _kHandleSize,
child: CustomPaint(
painter: _TextSelectionHandlePainter(
color: Theme.of(context).textSelectionHandleColor
),
),
);
// [handle] is a circle, with a rectangle in the top left quadrant of that
// circle (an onion pointing to 10:30). We rotate [handle] to point
// straight up or up-right depending on the handle type.
switch (type) {
case TextSelectionHandleType.left: // points up-right
return Transform.rotate(
angle: math.pi / 2.0,
child: handle,
);
case TextSelectionHandleType.right: // points up-left
return handle;
case TextSelectionHandleType.collapsed: // points up
return Transform.rotate(
angle: math.pi / 4.0,
child: handle,
);
}
assert(type != null);
return null;
}
/// Gets anchor for material-style text selection handles.
///
/// See [TextSelectionControls.getHandleAnchor].
#override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
switch (type) {
case TextSelectionHandleType.left:
return const Offset(_kHandleSize, 0);
case TextSelectionHandleType.right:
return Offset.zero;
default:
return const Offset(_kHandleSize / 2, -4);
}
}
#override
bool canSelectAll(TextSelectionDelegate delegate) {
// Android allows SelectAll when selection is not collapsed, unless
// everything has already been selected.
final TextEditingValue value = delegate.textEditingValue;
return delegate.selectAllEnabled &&
value.text.isNotEmpty &&
!(value.selection.start == 0 && value.selection.end == value.text.length);
}
}
/// Text selection controls that follow the Material Design specification.
final TextSelectionControls mymaterialTextSelectionControls = _MaterialTextSelectionControls();