Flutter - AnimatedScale not animating when widget update - flutter

I have a rating widget with 5 stars.
I can give a rating by dragging.
I want to animate the star with the star rating so that the scale gets bigger and the star without the star rating gets smaller again.
Therefore, I made sure that the star with the star rating is created using the AnimatedScale.
However, as you can see in the attached gif, the size of the star changes but it is not animated.
I gave the Duration value of the AnimatedScale property to 2 seconds, but it is being changed as soon as it is graded.
How can the stars grow and shrink smoothly?
Here is my code
import 'package:flutter/material.dart';
typedef void RatingChangeCallback(double rating);
class SmoothStarRating extends StatelessWidget {
final int starCount;
final double rating;
final RatingChangeCallback onRatingChanged;
final Color? color;
final Color? borderColor;
final double size;
final double spacing;
SmoothStarRating({
this.starCount = 5,
this.spacing = 0.0,
this.rating = 0.0,
required this.onRatingChanged,
this.color,
this.borderColor,
this.size = 25,
});
Widget getIcon(int starIndex) {
if (starIndex >= rating) {
return Icon(Icons.star_border, color: borderColor, size: size);
} else if (starIndex > rating - 0.5 && starIndex < rating) {
return AnimatedScale(
duration: Duration(seconds: 2),
scale: 1.1,
child: Icon(Icons.star_half, color: color, size: size),
);
} else {
return AnimatedScale(
duration: Duration(seconds: 2),
scale: 1.1,
child: Icon(Icons.star, color: color, size: size),
);
}
}
Widget buildStar(BuildContext context, int starIndex) {
return GestureDetector(
onTap: () {
onRatingChanged(starIndex + 1.0);
},
onHorizontalDragUpdate: (dragDetails) {
RenderBox box = context.findRenderObject() as RenderBox;
var _pos = box.globalToLocal(dragDetails.globalPosition);
var newRating = _pos.dx / size;
if (newRating > starCount) newRating = starCount.toDouble();
if (newRating < 0) newRating = 0.0;
onRatingChanged(newRating);
},
child: getIcon(starIndex),
);
}
#override
Widget build(BuildContext context) {
return Wrap(
alignment: WrapAlignment.start,
spacing: spacing,
children: List.generate(starCount, (starIndex) => buildStar(context, starIndex)),
);
}
}
+ ADDITIONAL
I tried implementing it using animation_controller, but the results are the same.
import 'package:flutter/material.dart';
typedef void RatingChangeCallback(double rating);
class SmoothStarRating extends StatefulWidget {
final int starCount;
final double rating;
final RatingChangeCallback onRatingChanged;
final Color? color;
final Color? borderColor;
final double size;
final double spacing;
SmoothStarRating({
this.starCount = 5,
this.spacing = 0.0,
this.rating = 0.0,
required this.onRatingChanged,
this.color,
this.borderColor,
this.size = 25,
});
#override
State<SmoothStarRating> createState() => _SmoothStarRatingState();
}
class _SmoothStarRatingState extends State<SmoothStarRating> with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
)..forward();
late final Animation<double> _animation = Tween(begin: 1.0, end: 1.1).animate(_controller);
#override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget getIcon(int starIndex) {
if (starIndex >= widget.rating) {
return Icon(Icons.star_border, color: widget.borderColor, size: widget.size);
} else if (starIndex > widget.rating - 0.5 && starIndex < widget.rating) {
return Icon(Icons.star_half, color: widget.color, size: widget.size);
} else {
return ScaleTransition(
scale: _animation,
child: Icon(Icons.star, color: widget.color, size: widget.size),
);
}
}
Widget buildStar(BuildContext context, int starIndex) {
return GestureDetector(
onTap: () {
widget.onRatingChanged(starIndex + 1.0);
},
onHorizontalDragUpdate: (dragDetails) {
RenderBox box = context.findRenderObject() as RenderBox;
var _pos = box.globalToLocal(dragDetails.globalPosition);
var newRating = _pos.dx / widget.size;
if (newRating > widget.starCount) newRating = widget.starCount.toDouble();
if (newRating < 0) newRating = 0.0;
widget.onRatingChanged(newRating);
},
child: getIcon(starIndex),
);
}
#override
Widget build(BuildContext context) {
return Wrap(
alignment: WrapAlignment.start,
spacing: widget.spacing,
children: List.generate(widget.starCount, (starIndex) => buildStar(context, starIndex)),
);
}
}

Related

Animation affect other animations in Flutter CustomPainter

So, i can't achieve two independents animation in customPainter. The animation of the invaders is affecting the animation of the spaceship.The animation of the invaders consist of a TweenSequence, that produces an effect of move and stop just like space invaders original game.
But the movement of the spaceShip should be continuos. The problem is that the space ship also is having the movement pattern of the invaders, eventhough the animation has a different Tween and a different controller.
The curious thing is that if I comment all the lines that draw the invaders( so the animation value is not being used) the spaceship has a continuos movement(the movement expected)
Just to clarify:
The animation controller and animation of the spaceship are: controllerAsync and animationAsync of SpaceInvadersMainView.dart
Here is the code:
SpaceInvadersMainView.dart
import 'package:croco/SpaceInvaders/InvadersPositionManager.dart';
import '../SpaceInvaders/InvadersConstruction.dart';
import 'package:flutter/material.dart';
import '../SpaceInvaders/InvadersAnimationManager.dart';
class SpaceInvadersMainView extends StatelessWidget {
const SpaceInvadersMainView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Space Invaders',
theme: ThemeData(
scaffoldBackgroundColor: Colors.black,
fontFamily: 'Segoe UI',
primarySwatch: Colors.lightBlue,
),
home: SpaceCanvas(),
);
}
}
class SpaceCanvas extends StatefulWidget {
SpaceCanvas({Key? key}) : super(key: key);
#override
State<SpaceCanvas> createState() => _SpaceCanvasState();
}
class _SpaceCanvasState extends State<SpaceCanvas> with TickerProviderStateMixin {
late AnimationController controller;
double animationStateValue = 0;
String keyLabel = "";
late AnimationController controllerAsync;
late double animationStateValueAsync;
late var animation = TweenSequence<double>(InvadersAnimationManager.getTweenRightMovement(-600, 500, 24)).animate(controller);
late var animationAsync = Tween<double>(begin: -700, end: 700).animate(controllerAsync);
#override
void initState() {
super.initState();
controllerAsync = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 10000)
);
animationAsync
.addListener(() {
setState(() {
print(animationAsync.value);
});
});
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 10000 )
);
animation
.addListener(() {
setState(() {
});
});
controller.forward();
controllerAsync.forward();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return RawKeyboardListener(
autofocus: true,
focusNode: FocusNode(),
onKey: (event) {
keyLabel = event.logicalKey.keyLabel;
},
child: Scaffold(
body: Center(
child: Column(
children: <Widget>[
Align(
alignment: Alignment.topCenter,
child: Container(
padding: const EdgeInsets.only(top: 20),
child: const Text(
'Welcome to space Invaders!',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 25,
color: Colors.white70
)
),
),
),
Stack(
children: <Widget>[
CustomPaint(
painter : InvadersPaint(animation, animationAsync)
),
],
)
])
)
),
);
}
}
class InvadersPaint extends CustomPainter {
Paint basicSquarePaint = Paint();
Path originPath = Path();
late Animation animation;
late double animationStateValue;
late Animation animationAsync;
InvadersPaint(this.animation, this.animationAsync) : super(repaint: animation);
#override
void paint(Canvas canvas, Size size) {
Path testPath = Path();
Paint paint = Paint();
paint.color = Colors.greenAccent;
testPath = InvadersConstruction.drawInvader(animationAsync.value, 670, "spaceShip");
canvas.drawPath(testPath, paint);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value + 6 + 2, 100, "squid", 58, Colors.purpleAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value + 2, 144, "crab", 58, Colors.lightBlueAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value + 2, 188, "crab", 58, Colors.lightBlueAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value, 232, "octopus", 58, Colors.yellowAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value, 276, "octopus", 58, Colors.yellowAccent);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
InvadersContruction.dart
import 'package:flutter/material.dart';
class InvadersConstruction {
static Path drawInvader(double originX, double originY, String typeOfInvader) {
var transitionPath = Path();
var pathList = <List<int>>[];
const List<List<int>> octopusMatrix = [
[1,1,0,0,0,0,0,0,0,0,1,1],
[0,0,1,1,0,1,1,0,1,1,0,0],
[0,0,0,1,1,0,0,1,1,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,0,0,1,1,0,0,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1],
[0,1,1,1,1,1,1,1,1,1,1,0],
[0,0,0,0,1,1,1,1,0,0,0,0],
];
const List<List<int>> crabMatrix = [
[0,0,0,1,1,0,1,1,0,0,0],
[1,0,1,0,0,0,0,0,1,0,1],
[1,0,1,1,1,1,1,1,1,0,1],
[1,1,1,1,1,1,1,1,1,1,1],
[0,1,1,0,1,1,1,0,1,1,0],
[0,0,1,1,1,1,1,1,1,0,0],
[0,0,0,1,0,0,0,1,0,0,0],
[0,0,1,0,0,0,0,0,1,0,0]
];
const List<List<int>> squidMatrix = [
[1,0,1,0,0,1,0,1],
[0,1,0,1,1,0,1,0],
[0,0,1,0,0,1,0,0],
[1,1,1,1,1,1,1,1],
[1,1,0,1,1,0,1,1],
[0,1,1,1,1,1,1,0],
[0,0,1,1,1,1,0,0],
[0,0,0,1,1,0,0,0]
];
const List<List<int>> spaceShipMatrix = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
[0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
];
void drawSquare( int row, int isItSquare, int sqrPosition ) {
double y;
double x;
Path secondaryPath = Path();
y = originY + row * -4;
if (sqrPosition == 0) {
x = originX;
} else {
x = sqrPosition * 4 + originX;
}
if (isItSquare == 1) {
secondaryPath.moveTo(x, y);
secondaryPath.relativeLineTo(4, 0);
secondaryPath.relativeLineTo(0, -4);
secondaryPath.relativeLineTo(-4, 0);
secondaryPath.close();
transitionPath = Path.combine(PathOperation.union, transitionPath, secondaryPath);
transitionPath.fillType = PathFillType.evenOdd;
}
}
int counterRow = -1;
int counterCol = -1;
if(typeOfInvader == "octopus") {
pathList = octopusMatrix;
} else if(typeOfInvader == "crab") {
pathList = crabMatrix;
} else if(typeOfInvader == "squid") {
pathList = squidMatrix;
} else if(typeOfInvader == "spaceShip") {
pathList = spaceShipMatrix;
}
for ( var row in pathList) {
counterRow++;
// print("This is the counter of the row: $counterRow");
for (var sqr in row) {
counterCol++;
// print("This is the counter of the square position: $counterCol");
drawSquare(counterRow, sqr, counterCol);
if (counterCol == row.length -1) {
counterCol = -1;
}
}
}
return transitionPath;
}
}
InvadersAnimationManager.dart
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'dart:html';
class InvadersAnimationManager {
//This method maps the right direction movement(-x -> x) of invaders in a List of Tween Sequence Items, that it's
//going to be procesed by the TweenSequence constructor '../Views/SpaceInvadersMainView.dart' line: 37
static List<TweenSequenceItem<double>> getTweenRightMovement (double originX, double finalX, double pixelsToRight) {
double steps = 16;
var tweenSequence = <TweenSequenceItem<double>>[];
for(int i = 1; i <= steps; i++ ) {
var subCounter = i - 1;
var tweenItem = TweenSequenceItem<double>(
tween: Tween<double>(begin: originX + pixelsToRight * subCounter, end: originX + pixelsToRight * i),
weight: 100/ steps
);
tweenSequence.add(tweenItem);
}
return tweenSequence;
}
}
InvadersPositionManager.dart
import 'package:flutter/material.dart';
import 'InvadersConstruction.dart';
class InvadersPositionManager {
static void placeInvadersOnLine(Canvas canvas, double originX, double originY, String invader, double inBetweenPixels, Color color) {
int _rowElements = 12;
Path _internalInvader;
Paint _paint = Paint();
double _invaderLength = 48;
_paint.color = color;
for(int i = 1; i <= 12 + 1; i++) {
if(i == 1) {
_internalInvader = InvadersConstruction.drawInvader(originX, originY, invader);
} else {
_internalInvader = InvadersConstruction.drawInvader(originX + _invaderLength + i * inBetweenPixels, originY, invader);
canvas.drawPath(_internalInvader, _paint);
}
}
}
}

Get widget position in a CustomScrollView

How to get the widget position when it is in a CustomScrollView > SliverToBoxAdapter. My widget ZoomItem zoom by creating a new widget with overlay, but when I scroll up or down, or when I change the size of the window the overlay appear aside. Why "final Offset offset = renderBox.localToGlobal(Offset.zero);" still return the same position? Is there a way to update the renderBox? Why there is no update of the offset in my code?
ZoomItem:
class ZoomItem extends ConsumerStatefulWidget {
final Widget child;
const ZoomItem({Key? key, required this.child}) : super(key: key);
#override
_ZoomItemState createState() => _ZoomItemState();
}
class _ZoomItemState extends ConsumerState<ZoomItem>
with SingleTickerProviderStateMixin {
OverlayEntry? overlayEntry;
late bool hasReachedTop;
late double height, width, xPosition, yPosition;
late AnimationController _animationController;
late Animation<double> animTween;
#override
void initState() {
super.initState();
hasReachedTop = true;
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 400));
_animationController.addListener(() {
if (_animationController.isDismissed) {
closeDropDown();
}
});
animTween = Tween<double>(begin: 1.0, end: 2.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
final scrollController = ref.read(scrollControllerProvider);
scrollController.addListener(() {
if (scrollController.position.atEdge) {
if (scrollController.position.pixels == 0) {
hasReachedTop = true;
} else {
hasReachedTop = false;
}
}
});
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (onEnter) {
insertOverlay(context);
},
onExit: (onExit) {
if (!_animationController.isAnimating) {
closeDropDown();
}
},
child: widget.child);
}
void insertOverlay(BuildContext context) {
closeDropDown();
findDropdownData(context);
overlayEntry = _createOverlay();
Overlay.of(context)!.insert(overlayEntry!);
}
OverlayEntry _createOverlay() {
return OverlayEntry(builder: (context) {
return Stack(
alignment: Alignment.center,
children: [
Positioned(
top: yPosition,
left: xPosition,
height: height,
width: width,
child: MouseRegion(
onEnter: (onEnter) {
_animationController.forward();
},
onExit: (onExit) {
_animationController.reverse();
},
child:
ScaleTransition(scale: animTween, child: widget.child))),
],
);
});
}
void findDropdownData(BuildContext context) {
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
height = renderBox.size.height;
width = renderBox.size.width;
final Offset offset = renderBox.localToGlobal(Offset.zero);
xPosition = offset.dx - kPaddingHorizontal;
final top = hasReachedTop ? kSliverTop : 0;
yPosition = offset.dy - top;
}
void closeDropDown() {
if (overlayEntry != null) {
overlayEntry!.remove();
overlayEntry = null;
}
}
}
GridViewStaggered:
class GridViewStaggered extends StatelessWidget {
final ResponsiveLayoutSize currentSize;
final List<Item> listItem;
const GridViewStaggered({Key? key, required this.currentSize, required this.listItem}) : super(key: key);
#override
Widget build(BuildContext context) {
return StaggeredGrid.count(
children: [
for(int i = 0 ; i < listItem.length; i++)
StaggeredGridTile.count(
crossAxisCellCount: i % 2 == 0 ? 2:1,
mainAxisCellCount: i % 2 == 0 ? 2:1,
child: i.isEven? ItemCard(
item: listItem[i],
):ZoomItem(
child: ItemCard(
item: listItem[i],
),
),
)
],
crossAxisCount: 4);
}
}

Flutter chat text align like Whatsapp or Telegram

I'm having trouble figuring this alignment with Flutter.
The right conversion on Whatsapp or Telegram is left-aligned but the date is on the right. If there's space available for the date it is at the end of the same line.
The 1st and 3rd chat lines can be done with Wrap() widget. But the 2nd line is not possible with Wrap() since the chat text is a separate Widget and fills the full width and doesn't allow the date widget to fit. How would you do this with Flutter?
Here's an example that you can run in DartPad that might be enough to get you started. It uses a SingleChildRenderObjectWidget for laying out the child and painting the ChatBubble's chat message as well as the message time and a dummy check mark icon.
To learn more about the RenderObject class I can recommend this video. It describes all relevant classes and methods in great depth and helped me a lot to create my first custom RenderObject.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:intl/intl.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: ExampleChatBubbles(),
),
),
);
}
}
class ChatBubble extends StatelessWidget {
final String message;
final DateTime messageTime;
final Alignment alignment;
final Icon icon;
final TextStyle textStyleMessage;
final TextStyle textStyleMessageTime;
// The available max width for the chat bubble in percent of the incoming constraints
final int maxChatBubbleWidthPercentage;
const ChatBubble({
Key? key,
required this.message,
required this.icon,
required this.alignment,
required this.messageTime,
this.maxChatBubbleWidthPercentage = 80,
this.textStyleMessage = const TextStyle(
fontSize: 11,
color: Colors.black,
),
this.textStyleMessageTime = const TextStyle(
fontSize: 11,
color: Colors.black,
),
}) : assert(
maxChatBubbleWidthPercentage <= 100 &&
maxChatBubbleWidthPercentage >= 50,
'maxChatBubbleWidthPercentage width must lie between 50 and 100%',
),
super(key: key);
#override
Widget build(BuildContext context) {
final textSpan = TextSpan(text: message, style: textStyleMessage);
final textPainter = TextPainter(
text: textSpan,
textDirection: ui.TextDirection.ltr,
);
return Align(
alignment: alignment,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 5,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.green.shade200,
),
child: InnerChatBubble(
maxChatBubbleWidthPercentage: maxChatBubbleWidthPercentage,
textPainter: textPainter,
child: Padding(
padding: const EdgeInsets.only(
left: 15,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
DateFormat('hh:mm').format(messageTime),
style: textStyleMessageTime,
),
const SizedBox(
width: 5,
),
icon
],
),
),
),
),
);
}
}
// By using a SingleChildRenderObjectWidget we have full control about the whole
// layout and painting process.
class InnerChatBubble extends SingleChildRenderObjectWidget {
final TextPainter textPainter;
final int maxChatBubbleWidthPercentage;
const InnerChatBubble({
Key? key,
required this.textPainter,
required this.maxChatBubbleWidthPercentage,
Widget? child,
}) : super(key: key, child: child);
#override
RenderObject createRenderObject(BuildContext context) {
return RenderInnerChatBubble(textPainter, maxChatBubbleWidthPercentage);
}
#override
void updateRenderObject(
BuildContext context, RenderInnerChatBubble renderObject) {
renderObject
..textPainter = textPainter
..maxChatBubbleWidthPercentage = maxChatBubbleWidthPercentage;
}
}
class RenderInnerChatBubble extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
TextPainter _textPainter;
int _maxChatBubbleWidthPercentage;
double _lastLineHeight = 0;
RenderInnerChatBubble(
TextPainter textPainter, int maxChatBubbleWidthPercentage)
: _textPainter = textPainter,
_maxChatBubbleWidthPercentage = maxChatBubbleWidthPercentage;
TextPainter get textPainter => _textPainter;
set textPainter(TextPainter value) {
if (_textPainter == value) return;
_textPainter = value;
markNeedsLayout();
}
int get maxChatBubbleWidthPercentage => _maxChatBubbleWidthPercentage;
set maxChatBubbleWidthPercentage(int value) {
if (_maxChatBubbleWidthPercentage == value) return;
_maxChatBubbleWidthPercentage = value;
markNeedsLayout();
}
#override
void performLayout() {
// Layout child and calculate size
size = _performLayout(
constraints: constraints,
dry: false,
);
// Position child
final BoxParentData childParentData = child!.parentData as BoxParentData;
childParentData.offset = Offset(
size.width - child!.size.width, textPainter.height - _lastLineHeight);
}
#override
Size computeDryLayout(BoxConstraints constraints) {
return _performLayout(constraints: constraints, dry: true);
}
Size _performLayout({
required BoxConstraints constraints,
required bool dry,
}) {
final BoxConstraints constraints =
this.constraints * (_maxChatBubbleWidthPercentage / 100);
textPainter.layout(minWidth: 0, maxWidth: constraints.maxWidth);
double height = textPainter.height;
double width = textPainter.width;
// Compute the LineMetrics of our textPainter
final List<ui.LineMetrics> lines = textPainter.computeLineMetrics();
// We are only interested in the last line's width
final lastLineWidth = lines.last.width;
_lastLineHeight = lines.last.height;
// Layout child and assign size of RenderBox
if (child != null) {
late final Size childSize;
if (!dry) {
child!.layout(BoxConstraints(maxWidth: constraints.maxWidth),
parentUsesSize: true);
childSize = child!.size;
} else {
childSize =
child!.getDryLayout(BoxConstraints(maxWidth: constraints.maxWidth));
}
final horizontalSpaceExceeded =
lastLineWidth + childSize.width > constraints.maxWidth;
if (horizontalSpaceExceeded) {
height += childSize.height;
_lastLineHeight = 0;
} else {
height += childSize.height - _lastLineHeight;
}
if (lines.length == 1 && !horizontalSpaceExceeded) {
width += childSize.width;
}
}
return Size(width, height);
}
#override
void paint(PaintingContext context, Offset offset) {
// Paint the chat message
textPainter.paint(context.canvas, offset);
if (child != null) {
final parentData = child!.parentData as BoxParentData;
// Paint the child (i.e. the row with the messageTime and Icon)
context.paintChild(child!, offset + parentData.offset);
}
}
}
class ExampleChatBubbles extends StatelessWidget {
// Some chat dummy data
final chatData = [
[
'Hi',
Alignment.centerRight,
DateTime.now().add(const Duration(minutes: -100)),
],
[
'Helloooo?',
Alignment.centerRight,
DateTime.now().add(const Duration(minutes: -60)),
],
[
'Hi James',
Alignment.centerLeft,
DateTime.now().add(const Duration(minutes: -58)),
],
[
'Do you want to watch the basketball game tonight? We could order some chinese food :)',
Alignment.centerRight,
DateTime.now().add(const Duration(minutes: -57)),
],
[
'Sounds great! Let us meet at 7 PM, okay?',
Alignment.centerLeft,
DateTime.now().add(const Duration(minutes: -57)),
],
[
'See you later!',
Alignment.centerRight,
DateTime.now().add(const Duration(minutes: -55)),
],
];
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount: chatData.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 5,
),
child: ChatBubble(
icon: Icon(
Icons.check,
size: 15,
color: Colors.grey.shade700,
),
alignment: chatData[index][1] as Alignment,
message: chatData[index][0] as String,
messageTime: chatData[index][2] as DateTime,
// How much of the available width may be consumed by the ChatBubble
maxChatBubbleWidthPercentage: 75,
),
);
},
),
);
}
}
#hnnngwdlch thanks for your answer it helped me, with this you have full control over the painter. I slightly modified your code for my purposes maybe it will be useful for someone.
PD: I don't know if declaring the TextPainter inside the RenderObject has significant performance disadvantages, if someone knows please write in the comments.
class TextMessageWidget extends SingleChildRenderObjectWidget {
final String text;
final TextStyle? textStyle;
final double? spacing;
const TextMessageWidget({
Key? key,
required this.text,
this.textStyle,
this.spacing,
required Widget child,
}) : super(key: key, child: child);
#override
RenderObject createRenderObject(BuildContext context) {
return RenderTextMessageWidget(text, textStyle, spacing);
}
#override
void updateRenderObject(BuildContext context, RenderTextMessageWidget renderObject) {
renderObject
..text = text
..textStyle = textStyle
..spacing = spacing;
}
}
class RenderTextMessageWidget extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
String _text;
TextStyle? _textStyle;
double? _spacing;
// With this constants you can modify the final result
static const double _kOffset = 1.5;
static const double _kFactor = 0.8;
RenderTextMessageWidget(
String text,
TextStyle? textStyle,
double? spacing
) : _text = text, _textStyle = textStyle, _spacing = spacing;
String get text => _text;
set text(String value) {
if (_text == value) return;
_text = value;
markNeedsLayout();
}
TextStyle? get textStyle => _textStyle;
set textStyle(TextStyle? value) {
if (_textStyle == value) return;
_textStyle = value;
markNeedsLayout();
}
double? get spacing => _spacing;
set spacing(double? value) {
if (_spacing == value) return;
_spacing = value;
markNeedsLayout();
}
TextPainter textPainter = TextPainter();
#override
void performLayout() {
size = _performLayout(constraints: constraints, dry: false);
final BoxParentData childParentData = child!.parentData as BoxParentData;
childParentData.offset = Offset(
size.width - child!.size.width,
size.height - child!.size.height / _kOffset
);
}
#override
Size computeDryLayout(BoxConstraints constraints) {
return _performLayout(constraints: constraints, dry: true);
}
Size _performLayout({required BoxConstraints constraints, required bool dry}) {
textPainter = TextPainter(
text: TextSpan(text: _text, style: _textStyle),
textDirection: TextDirection.ltr
);
late final double spacing;
if(_spacing == null){
spacing = constraints.maxWidth * 0.03;
} else {
spacing = _spacing!;
}
textPainter.layout(minWidth: 0, maxWidth: constraints.maxWidth);
double height = textPainter.height;
double width = textPainter.width;
// Compute the LineMetrics of our textPainter
final List<LineMetrics> lines = textPainter.computeLineMetrics();
// We are only interested in the last line's width
final lastLineWidth = lines.last.width;
if(child != null){
late final Size childSize;
if (!dry) {
child!.layout(BoxConstraints(maxWidth: constraints.maxWidth), parentUsesSize: true);
childSize = child!.size;
} else {
childSize = child!.getDryLayout(BoxConstraints(maxWidth: constraints.maxWidth));
}
if(lastLineWidth + spacing > constraints.maxWidth - child!.size.width) {
height += (childSize.height * _kFactor);
} else if(lines.length == 1){
width += childSize.width + spacing;
}
}
return Size(width, height);
}
#override
void paint(PaintingContext context, Offset offset) {
textPainter.paint(context.canvas, offset);
final parentData = child!.parentData as BoxParentData;
context.paintChild(child!, offset + parentData.offset);
}
}

Flutter DropDown button for account settings

Want to design a custom drop down button for account settings with logout option.
How to change drop down list background color?
How to customize drop down list?
DropdownButton(
hint: ButtonText(
buttonText: "Select a option",
textColor: ThemeColors.grey8,
buttonFontSize: 20.0,
),
value: _selectedOption,
onChanged: (value) {
setState(() {
_selectedOption = value;
});
},
items: _options.map((option) {
return DropdownMenuItem(
child: ButtonText(
buttonText: option,
buttonFontSize: 16.0,
textColor: ThemeColors.grey8,
),
value: option,
);
}).toList(),
i want design a custom drop down button with a account dashboard.
I had a similar situation. I needed to implement a dropdown dialog. I used the DropdownButton sources and modified them.
My widget:
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
const double _hintWidgetHeight = 34;
const double _hintWidgetWidth = 140;
class MyHintWidget extends StatefulWidget {
const MyHintWidget({
Key key,
#required this.title,
#required this.style,
#required this.child,
}) : super(key: key);
final String title;
final TextStyle style;
final Widget child;
#override
_MyHintState createState() => _MyHintState();
}
class _MyHintState extends State<MyHintWidget> with WidgetsBindingObserver {
static const EdgeInsets _hintItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
_HintRoute route;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_removeRoute();
super.dispose();
}
#override
void didChangeMetrics() {
_removeRoute();
}
void _removeRoute() {
route?._dismiss();
route = null;
}
void _handleTap() {
final RenderBox itemBox = context.findRenderObject() as RenderBox;
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
final TextDirection textDirection = Directionality.of(context);
route = _HintRoute(
title: widget.title,
style: widget.style,
buttonRect: _hintItemPadding.resolve(textDirection).inflateRect(itemRect),
padding: _hintItemPadding.resolve(textDirection),
theme: ThemeData(
brightness: Brightness.light,
),
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
);
Navigator.push(context, route).then<void>((_) {
route = null;
});
}
#override
Widget build(BuildContext context) {
return Semantics(
button: true,
child: GestureDetector(
onTap: _handleTap,
behavior: HitTestBehavior.opaque,
child: widget.child,
),
);
}
}
class _HintRoute extends PopupRoute<void> {
_HintRoute({
#required this.title,
#required this.style,
this.padding,
this.buttonRect,
this.theme,
this.barrierLabel,
});
final String title;
final TextStyle style;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final ThemeData theme;
#override
Duration get transitionDuration => const Duration(milliseconds: 300);
#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,
) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return _HintRoutePage(
title: title,
style: style,
route: this,
constraints: constraints,
padding: padding,
buttonRect: buttonRect,
theme: theme,
);
});
}
void _dismiss() => navigator?.removeRoute(this);
}
class _HintRoutePage extends StatelessWidget {
const _HintRoutePage({
Key key,
#required this.title,
#required this.style,
this.route,
this.constraints,
this.padding,
this.buttonRect,
this.theme,
}) : super(key: key);
final String title;
final TextStyle style;
final _HintRoute route;
final BoxConstraints constraints;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final ThemeData theme;
#override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final double availableHeight = constraints.maxHeight;
final double maxHintHeight = availableHeight - 2.0 * _hintWidgetHeight;
final double buttonTop = buttonRect.top;
final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
final double topLimit = math.min(_hintWidgetHeight, buttonTop);
final double bottomLimit = math.max(availableHeight - _hintWidgetHeight, buttonBottom);
final double selectedItemOffset = _hintWidgetHeight + kMaterialListPadding.top;
double hintTop =
(buttonTop - selectedItemOffset) - (_hintWidgetHeight * 2 - buttonRect.height) / 3;
final double preferredHintHeight = _hintWidgetHeight + kMaterialListPadding.vertical;
final double hintHeight = math.min(maxHintHeight, preferredHintHeight);
double hintBottom = hintTop + hintHeight;
if (hintTop < topLimit) hintTop = math.min(buttonTop, topLimit);
if (hintBottom > bottomLimit) {
hintBottom = math.max(buttonBottom, bottomLimit);
hintTop = hintBottom - hintHeight;
}
final TextDirection textDirection = Directionality.of(context);
Widget hint = _HintWidget(
title: title,
style: style,
route: route,
padding: padding.resolve(textDirection),
);
if (theme != null) hint = Theme(data: theme, child: hint);
return MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
child: Builder(
builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _HintRouteLayout(
buttonRect: buttonRect,
hintTop: hintTop,
hintHeight: hintHeight,
textDirection: textDirection,
),
child: hint,
);
},
),
);
}
}
class _HintRouteLayout extends SingleChildLayoutDelegate {
_HintRouteLayout({
#required this.buttonRect,
#required this.hintTop,
#required this.hintHeight,
#required this.textDirection,
});
final Rect buttonRect;
final double hintTop;
final double hintHeight;
final TextDirection textDirection;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
final double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _hintWidgetHeight);
const double width = _hintWidgetWidth;
return BoxConstraints(
minWidth: width,
maxWidth: width,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
assert(() {
final Rect container = Offset.zero & size;
if (container.intersect(buttonRect) == buttonRect) {
assert(hintTop >= 0.0);
assert(hintTop + hintHeight <= size.height);
}
return true;
}());
assert(textDirection != null);
double left;
if (buttonRect.left > size.width - childSize.width / 1.1) {
left = size.width - childSize.width;
} else if (buttonRect.left < childSize.width / 5) {
left = 0;
} else {
left = buttonRect.left - (buttonRect.right - buttonRect.left) / 3;
}
return Offset(left, hintTop);
}
#override
bool shouldRelayout(_HintRouteLayout oldDelegate) {
return buttonRect != oldDelegate.buttonRect ||
hintTop != oldDelegate.hintTop ||
hintHeight != oldDelegate.hintHeight ||
textDirection != oldDelegate.textDirection;
}
}
class _HintWidget extends StatefulWidget {
const _HintWidget({
Key key,
#required this.title,
#required this.style,
this.padding,
this.route,
}) : super(key: key);
final String title;
final TextStyle style;
final _HintRoute route;
final EdgeInsets padding;
#override
_HintState createState() => _HintState();
}
class _HintState extends State<_HintWidget> {
CurvedAnimation _fadeOpacity;
CurvedAnimation _resize;
#override
void initState() {
super.initState();
_fadeOpacity = CurvedAnimation(
parent: widget.route.animation,
curve: const Interval(0.0, 0.25),
reverseCurve: const Interval(0.75, 1.0),
);
_resize = CurvedAnimation(
parent: widget.route.animation,
curve: const Interval(0.25, 0.5),
reverseCurve: const Threshold(0.0),
);
}
#override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final _HintRoute route = widget.route;
const double unit = 0.5 / 1.5;
CurvedAnimation opacity;
final double start = (0.5 + 1 * unit).clamp(0.0, 1.0) as double;
final double end = (start + 1.5 * unit).clamp(0.0, 1.0) as double;
opacity = CurvedAnimation(parent: route.animation, curve: Interval(start, end));
return FadeTransition(
opacity: _fadeOpacity,
child: CustomPaint(
painter: _HintPainter(
color: Theme.of(context).canvasColor,
elevation: 8,
resize: _resize,
),
child: Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: localizations.popupMenuLabel,
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: kMaterialListPadding,
child: FadeTransition(
opacity: opacity,
child: InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: widget.padding,
child: Text(
widget.title,
style: widget.style,
textAlign: TextAlign.center,
),
),
),
),
),
),
),
),
);
}
}
class _HintPainter extends CustomPainter {
_HintPainter({
this.color,
this.elevation,
this.resize,
}) : _painter = BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6.0),
boxShadow: kElevationToShadow[elevation],
).createBoxPainter(),
super(repaint: resize);
final Color color;
final int elevation;
final Animation<double> resize;
final BoxPainter _painter;
#override
void paint(Canvas canvas, Size size) {
final double selectedItemOffset = _hintWidgetHeight + kMaterialListPadding.top;
final Tween<double> top = Tween<double>(
begin: selectedItemOffset.clamp(0.0, size.height - _hintWidgetHeight) as double,
end: 0.0,
);
final Tween<double> bottom = Tween<double>(
begin: (top.begin + _hintWidgetHeight).clamp(_hintWidgetHeight, size.height) as double,
end: size.height,
);
final Rect rect = Rect.fromLTRB(0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
_painter.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
}
#override
bool shouldRepaint(_HintPainter oldPainter) {
return oldPainter.color != color ||
oldPainter.elevation != elevation ||
oldPainter.resize != resize;
}
}
So it is used:
return MyHintWidget(
title: 'some text',
style: //text style[![enter image description here][1]][1],
child: //child,
);
Result

How to add Signature in flutter?

I have implemented signature_pad in my flutter project and it works fine.
Unfortunately when I place it inside SingleChildScrollView, the signature was not drawn. It scrolled instead of signed.
It seems like is the GestureDetector but I have no idea how to fix it.
Can someone give me some clue on this?
Thanks.
Signature Class need to be modified to respond to VerticalDrag , I renamed it to Signature1
now signature area pad should not scroll , you can check the complete code below as it behaves. you will find out that Signature area is no more scrolling with the SingleChildScrollView.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:async';
import 'dart:ui' as ui;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var color = Colors.black;
var strokeWidth = 3.0;
final _sign = GlobalKey<Signature1State>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body:
SingleChildScrollView(
child: Column(
children: <Widget>[
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
_showSignaturePad()
],
),
)
,
);
}
Widget _showCategory() {
return TextField(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
style: TextStyle(fontSize: 12.0, height: 1.0),
decoration: InputDecoration(hintText: "TextView"));
}
Widget _showSignaturePad() {
return Container(
width: double.infinity,
height: 200,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 200,
//color: Colors.red,
child: Signature1(
color: color,
key: _sign,
strokeWidth: strokeWidth,
),
),
),
color: Colors.grey.shade300,
);
}
}
class Signature1 extends StatefulWidget {
final Color color;
final double strokeWidth;
final CustomPainter backgroundPainter;
final Function onSign;
Signature1({
this.color = Colors.black,
this.strokeWidth = 5.0,
this.backgroundPainter,
this.onSign,
Key key,
}) : super(key: key);
Signature1State createState() => Signature1State();
static Signature1State of(BuildContext context) {
return context.findAncestorStateOfType<Signature1State>();
}
}
class _SignaturePainter extends CustomPainter {
Size _lastSize;
final double strokeWidth;
final List<Offset> points;
final Color strokeColor;
Paint _linePaint;
_SignaturePainter({#required this.points, #required this.strokeColor, #required this.strokeWidth}) {
_linePaint = Paint()
..color = strokeColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
}
#override
void paint(Canvas canvas, Size size) {
_lastSize = 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], _linePaint);
}
}
#override
bool shouldRepaint(_SignaturePainter other) => other.points != points;
}
class Signature1State extends State<Signature1> {
List<Offset> _points = <Offset>[];
_SignaturePainter _painter;
Size _lastSize;
Signature1State();
void _onDragStart(DragStartDetails details){
RenderBox referenceBox = context.findRenderObject();
Offset localPostion = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_points = List.from(_points)
..add(localPostion)
..add(localPostion);
});
}
void _onDragUpdate (DragUpdateDetails details) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_points = List.from(_points)..add(localPosition);
if (widget.onSign != null) {
widget.onSign();
}
});
}
void _onDragEnd (DragEndDetails details) => _points.add(null);
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
_painter = _SignaturePainter(points: _points, strokeColor: widget.color, strokeWidth: widget.strokeWidth);
return ClipRect(
child: CustomPaint(
painter: widget.backgroundPainter,
foregroundPainter: _painter,
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
onPanStart: _onDragStart,
onPanUpdate: _onDragUpdate,
onPanEnd: _onDragEnd
),
),
);
}
Future<ui.Image> getData() {
var recorder = ui.PictureRecorder();
var origin = Offset(0.0, 0.0);
var paintBounds = Rect.fromPoints(_lastSize.topLeft(origin), _lastSize.bottomRight(origin));
var canvas = Canvas(recorder, paintBounds);
if(widget.backgroundPainter != null) {
widget.backgroundPainter.paint(canvas, _lastSize);
}
_painter.paint(canvas, _lastSize);
var picture = recorder.endRecording();
return picture.toImage(_lastSize.width.round(), _lastSize.height.round());
}
void clear() {
setState(() {
_points = [];
});
}
bool get hasPoints => _points.length > 0;
List<Offset> get points => _points;
afterFirstLayout(BuildContext context) {
_lastSize = context.size;
}
}
you need to create a CustomGestureDetector.
Check this updated version of Signature that I just changed to you:
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class Signature extends StatefulWidget {
final Color color;
final double strokeWidth;
final CustomPainter backgroundPainter;
final Function onSign;
Signature({
this.color = Colors.black,
this.strokeWidth = 5.0,
this.backgroundPainter,
this.onSign,
Key key,
}) : super(key: key);
SignatureState createState() => SignatureState();
static SignatureState of(BuildContext context) {
return context.findAncestorStateOfType<SignatureState>();
}
}
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanStart;
final Function onPanUpdate;
final Function onPanEnd;
CustomPanGestureRecognizer({#required this.onPanStart, #required this.onPanUpdate, #required this.onPanEnd});
#override
void addPointer(PointerEvent event) {
onPanStart(event.position);
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
}
#override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
#override
String get debugDescription => 'customPan';
#override
void didStopTrackingLastPointer(int pointer) {}
}
class _SignaturePainter extends CustomPainter {
Size _lastSize;
final double strokeWidth;
final List<Offset> points;
final Color strokeColor;
Paint _linePaint;
_SignaturePainter({#required this.points, #required this.strokeColor, #required this.strokeWidth}) {
_linePaint = Paint()
..color = strokeColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
}
#override
void paint(Canvas canvas, Size size) {
_lastSize = 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], _linePaint);
}
}
#override
bool shouldRepaint(_SignaturePainter other) => other.points != points;
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
_SignaturePainter _painter;
Size _lastSize;
SignatureState();
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
_painter = _SignaturePainter(points: _points, strokeColor: widget.color, strokeWidth: widget.strokeWidth);
return ClipRect(
child: CustomPaint(
painter: widget.backgroundPainter,
foregroundPainter: _painter,
child: RawGestureDetector(
gestures: {
CustomPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
() => CustomPanGestureRecognizer(
onPanStart: (position) {
RenderBox referenceBox = context.findRenderObject();
Offset localPostion = referenceBox.globalToLocal(position);
setState(() {
_points = List.from(_points)..add(localPostion)..add(localPostion);
});
return true;
},
onPanUpdate: (position) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(position);
setState(() {
_points = List.from(_points)..add(localPosition);
if (widget.onSign != null) {
widget.onSign();
}
});
},
onPanEnd: (position) {
_points.add(null);
},
),
(CustomPanGestureRecognizer instance) {},
),
},
),
),
);
}
Future<ui.Image> getData() {
var recorder = ui.PictureRecorder();
var origin = Offset(0.0, 0.0);
var paintBounds = Rect.fromPoints(_lastSize.topLeft(origin), _lastSize.bottomRight(origin));
var canvas = Canvas(recorder, paintBounds);
if (widget.backgroundPainter != null) {
widget.backgroundPainter.paint(canvas, _lastSize);
}
_painter.paint(canvas, _lastSize);
var picture = recorder.endRecording();
return picture.toImage(_lastSize.width.round(), _lastSize.height.round());
}
void clear() {
setState(() {
_points = [];
});
}
bool get hasPoints => _points.length > 0;
List<Offset> get points => _points;
afterFirstLayout(BuildContext context) {
_lastSize = context.size;
}
}
Special attention to CustomPanGestureRecognizer
You can read more in:
Gesture Disambiguation
This is happening because the gesture from SingleChildScrollView overrides your Signature widget’s gesture as the SingleChildScrollView is the parent. There are few ways to solve it as in the other responses in this thread. But the easiest one is using the existing package. You can simply use the below Syncfusion's Flutter SignaturePad widget which I am using now for my application. This widget will work on Android, iOS, and web platforms.
Package - https://pub.dev/packages/syncfusion_flutter_signaturepad
Features - https://www.syncfusion.com/flutter-widgets/flutter-signaturepad
Documentation - https://help.syncfusion.com/flutter/signaturepad/getting-started