How to know if Offset is inside circle - flutter

In my code I have CircleAvatar with border. I want to know if user tap exactly on border. For this I need to check if tap is inside big circle (Container), but not inside small circle (CircleAvatar).
Does anybody know how could I check this?
Widget build(BuildContext context) {
return Listener(
child: Container(
key: key,
padding: EdgeInsets.all(8.0),
decoration: ShapeDecoration(shape: CircleBorder(), color: Colors.yellow),
child: CircleAvatar(
backgroundImage: NetworkImage(widget.imgSrc),
radius: 60.0,
),
),
onPointerDown: (event) {
if (renderBox == null) {
renderBox = key.currentContext?.findRenderObject();
}
Rect rect = renderBox.paintBounds;
// todo ......
},
);
}

You should use the Pythagore theorem to check if the Offset is in a circle :
import 'dart:math';
/// check if a [point] is in a circle of a given [radius]
bool isPointInside(Offset point, double radius) =>
pow(point.dx, 2) + pow(point.dy, 2) < pow(radius, 2);
Do this for the big and small circle and check if the results match what you want.
Edit : removed the sqrt in favour of pow(radius) as suggested by #randal-schwartz

Maybe it's not the best way, but I've found some solution
Rect bigRect = renderBox.paintBounds.shift(renderBox?.localToGlobal(Offset.zero));
Rect smallRect = bigRect.deflate(padding);
Path path = Path()
..fillType = PathFillType.evenOdd
..addOval(bigRect)
..addOval(smallRect);
if (path.contains(event.position)) { // todo...
P.S. Muldec's solution can be used too, it works, but I was looking for another type, like contains

Related

Flutter: How do I stretch the last Element in a Wrap Widget?

I think this doesn't need much explaining so I didn't include any code or screenshots.
As the title, suggests, I have a Wrap Widget and I want the last Element of it to take up all the available horizontal space to the right, (the same way as if you put an Expanded Widget in a Row).
I this case though, I can not use Expanded or Flexible, as Wrap doesn't allow that.
Edit, added a bit of (simplified) code and screenshots:
_buildTileDragTarget is just a function that returns a DragTarget.
void _buildAnswerWidgetsFromStrings() {
_answerWidgets.clear(); //start building from 0
_answerWidgets.add(_buildTileDragTarget(index: 0)); //add initial DragTarget
//for each word in _answerStrings, add Draggable Chip & DragTarget
for (int i = 0; i < _answerStrings.length; i++) {
WordTile wordTile = _answerStrings[i];
_answerWidgets.add(
Draggable<WordTile>(
key: UniqueKey(),
data: wordTile,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Chip(
label: Text(wordTile.word),
padding: EdgeInsets.all(0),
),
onTap: () {
//seems to work: tap from top to bottom (5)
setState(() {
_options[wordTile.word] = true;
_answerStrings.remove(wordTile);
});
},
),
feedback: Material(child: Chip(label: Text(wordTile.word))),
childWhenDragging: Chip(label: Text(' ' * wordTile.word.length)),
),
);
_answerWidgets.add(_buildTileDragTarget(index: i + 1)); //the position AFTER the current word (last)
}
//remove last DragTarget and add one wrapped in Expanded
_answerWidgets.removeLast();
_answerWidgets.add(
SizedBox(
// width: double.infinity,
child: _buildTileDragTarget(index: _answerStrings.length),
),
); //TODO try wrapping the whole thing in 1 big DragTarget (for last index)
// _answerWidgets.add(_buildTileDragTarget(index: _answerStrings.length));
}
Screenshots:
I wrapped the Wrap widget in a purple Container, each DragTarget is wrapped in a green Container. (you can ignore the blue part)
(1) This is what I have (without SizedBox)
(2) This is what I get when I try using SizedBox(width:double.infinity...)
(3) This is what I want
Try SizedBox(width: double.infinity)
There's also expand constructor you might use
SizedBox.Expand(
child: //your widget
)

Flutter: Layout multiple children with differing z coordinates from matrix transformations with correct overlap

I'm trying to build a Flutter Web version of the threejs Periodic Table Helix view (see here: https://mrdoob.com/lab/javascript/threejs/css3d/periodictable/ and click on "Helix")
Currently, I place all the element tiles in a stack, and position and rotate them with matrix transformations. See below code:
#override
Widget build(BuildContext context) {
double initialRotateX = (widget.atomicNumber - 1) *
(math.pi / Constants.numberOfElementsPerHalfCircle);
return Transform(
transform: Matrix4.identity()
..translate(
math.sin((widget.atomicNumber - 1) *
(math.pi / Constants.numberOfElementsPerHalfCircle)) *
Constants.radius,
widget.atomicNumber * 4,
-math.cos((widget.atomicNumber - 1) *
(math.pi / Constants.numberOfElementsPerHalfCircle)) *
Constants.radius,
)
..translate(Constants.elementCardHalfSize)
..rotateY(-initialRotateX)
..translate(-Constants.elementCardHalfSize),
child: Container(
decoration: BoxDecoration(
color: Constants.elementCardColor,
border: Border.all(width: 1, color: Colors.white.withOpacity(0.3))),
child: SizedBox(
width: Constants.elementCardWidth.toDouble(),
height: Constants.elementCardHeight.toDouble(),
child: ElementText()
),
),
);
All these cards are then placed inside a stack widget in another class, and that widget is rotated, like so:
return AnimatedBuilder(
animation: widget.animationControllerX,
builder: (context, _) {
double rotateX =
widget.animationControllerX.value / Constants.turnResolution;
double rotateY =
widget.animationControllerY.value / Constants.turnResolution;
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(-rotateX)
..rotateX(rotateY),
alignment: Alignment.center,
child: Container(
child: Stack(
children: List<Widget>.generate(
Constants.numberOfElements,
(index) => ElementCard(
atomicNumber:
Constants.elements[(index + 1).toString()].atomicNumber,
),
),
),
),
);
},
);
As you can see, there is a "..rotateY" transformation applied, which makes a card appear to move forward when the stack is rotated.
However, when rotating, cards that should be in the back are smaller, yet paint over the cards that should be in the front. (There is no doubt that the cards are in the right positions, since I can rotate them along the x axis and see that for myself, but when painting, the card on the back is painted over the text in the card on the front. I believe this is because a stack always positions the elevation of its widgets according to the order they're provided in, and the order is fixed in the list. Is there any way I can fix this?
Screenshots to explain what I mean:
I can change the opacity of the tiles to make the issue more obvious:
As you can see, the larger cards are closer to the screen, since that's how they're transformed, but they're painting behind the smaller cards. Any ideas on how to make this happen?
So I found a solution. It's not perfect, but it works so far.
The issue is that Flutter paints widgets in the order that they appear. Since the larger cards are higher up on the list, even though they have a closer z coordinate, they are painted first, and the smaller card is painted on top of them.
The solution is to use a Flow() widget to dynamically change the order in which the cards are painted. In the children widgets, where I'd normally build them, I instead stored their transformations in a map. Then, in the flow delegate function, access these transformations, calculate the z coordinate of each widget after transforming (row 3, column 3 (1 indexed) in the transformation matrix). Sort the widgets by z coordinate, and then paint them on screen in that order.
Here is the code for the same:
The part with the root Flow widget:
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: widget.animationControllerX,
builder: (context, _) {
double rotateX =
widget.animationControllerX.value / Constants.turnResolution;
double rotateY =
widget.animationControllerY.value / Constants.turnResolution;
return Container(
transform: Matrix4.identity()
..translate(
MediaQuery.of(context).size.width / 2,
),
child: Flow(
clipBehavior: Clip.none,
delegate: CustomFlowDelegate(rotateY: -rotateX, rotateX: rotateY),
children: List<Widget>.generate(
Constants.numberOfElements,
(index) => ElementCard(
atomicNumber:
Constants.elements[(index + 1).toString()].atomicNumber,
),
),
),
);
},
);
}
The FlowDelegate for the widget:
class CustomFlowDelegate extends FlowDelegate {
CustomFlowDelegate({this.rotateX, this.rotateY});
final rotateY, rotateX;
#override
void paintChildren(FlowPaintingContext context) {
Map<int, double> zValue = {};
for (int i = 0; i < context.childCount; i++) {
var map = Constants.transformationsMap[i + 1];
Matrix4 transformedMatrix = Matrix4.identity()
..setEntry(3, 2, 0.001)
..setEntry(3, 1, 0.001)
..rotateY(this.rotateY)
..rotateX(this.rotateX)
..translate(
map['translateOneX'], map['translateOneY'], map['translateOneZ'])
..translate(map['translateTwoX'])
..rotateY(map['rotateThreeY'])
..translate(map['translateFourX']);
zValue[i + 1] = transformedMatrix.getRow(2)[2];
}
var sortedKeys = zValue.keys.toList(growable: false)
..sort((k1, k2) => zValue[k1].compareTo(zValue[k2]));
LinkedHashMap sortedMap = new LinkedHashMap.fromIterable(sortedKeys,
key: (k) => k, value: (k) => zValue[k]);
for (int key in sortedMap.keys) {
var map = Constants.transformationsMap[key];
context.paintChild(
key - 1,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..setEntry(3, 1, 0.001)
..rotateY(this.rotateY)
..rotateX(this.rotateX)
..translate(
map['translateOneX'], map['translateOneY'], map['translateOneZ'])
..translate(map['translateTwoX'])
..rotateY(map['rotateThreeY'])
..translate(map['translateFourX']),
);
}
}
#override
bool shouldRepaint(covariant FlowDelegate oldDelegate) {
return true;
}
}
Here's what the result looks like:
And with the opacity turned up a bit so you can really see the fix in action:
And yes, I know storing the transformations in a map and then sorting them from another page isn't best practice.
Hope this helps someone.

Flutter. Set parent's height based on child canvas drawing's

Is there any way to set parent container's height based on height of what is drawn in the child canvas?
I am using a custom painter like this:
double width = MediaQuery.of(context).size.width;
return Container(
color: Colors.yellow,
height: 240,
width: width,
child: CustomPaint(
painter: ShapePainter(),
),
);
Then ShapePainter() draws different shapes (1 shape for each canvas in the list).
But as you can see, some shapes like 2nd rectangle, take twice the space they actually need.
I can calculate height of a shape inside ShapePainter() easily,
but I have no idea how to apply it to its parent's height. (Except calling an insta-setState() from child, but there should be a better way, because this may flicker for one frame)
Thanks for any help!
Flutter has several phases that it goes through when creating a frame, including building, layout, and painting. The size of widgets is determined during the layout phase. So you can't set the height based it what was painted. (Except, as you've said, by using something like setState to generate a new frame.) You'll have to determine the the size you want before any painting has happened. For example, you can give your CustomPainter a getter to provide the shape:
class ShapePainter extends CustomPainter {
ShapePainter({#required this.shape});
final Shape shape;
void paint(Canvas canvas, Size size) {
// the painting
}
Size get size {
// determine size based on shape
}
}
...
Widget build(BuildContext context) {
final shapePainter = ShapePainter();
return Container(
color: Colors.yellow,
height: shapePainter.size.height,
align: Alignment.center,
child: CustomPaint(
painter: shapePainter,
),
);
}

How do I make a child widget expand to fill a parent container inside of a stack when the child has no parameters to alter its layout?

I'm building a card game and using flame to pull the cards from a sprite sheet. The problem is that I set the width and height of the Container that holds the SpriteWidget, but the SpriteWidget expands to either the width or the height of the container, but not both. I want it to expand/stretch to be the same size as the parent container. Unfortunately, the SpriteWidget really has no parameters that could be used to change its size.
I've spent several hours scouring the internet for a solution and tried a number of widgets including FittedBox, Flex, Positioned.fill, etc., but I'm unable to achieve the desired effect. How can I make the SpriteWidget stretch to fill its parent when it has no parameters to do so?
class _PlayerHandPortraitLayout
extends WidgetView<PlayerHand, _PlayerHandController> {
#override
final state;
const _PlayerHandPortraitLayout(this.state) : super(state);
#override
Widget build(BuildContext build) {
return Stack(
children: state.displayHand().asMap().entries.map((cardItem) {
var index = cardItem.key;
var card = cardItem.value;
return Positioned(
left: index * CARD_OVERLAP_OFFSET,
child: Draggable<Container>(
childWhenDragging: Container(),
child: Container(
color: Colors.purple,
width: state.cardWidth,
height: state.cardHeight,
child: SpriteWidget(
sprite: state.spriteImages[card.suite.index][card.value.index],
),
),
feedback: Container(
color: Colors.yellow,
width: state.cardWidth,
height: state.cardHeight,
child: SpriteWidget(
sprite: state.spriteImages[card.suite.index][card.value.index],
),
),
),
);
}).toList(),
);
}
}
actually this will be not possible, SpriteWidget is designed to expand as long as it fits on the smallest dimension available on its parent, you can check on it source code here.
This is done so the Sprite will not get distorted when its parent has a different aspect ratio than the ratio of the Sprite.
If you have an use case where you would want the Sprite to get intentionally distorted, please open an issue on the Flame repository explaining the case, and we can try to take a look on it.

Flutter clip without space around the clipped widget

I want to clip a widget and use this clipped image in a layout and the bounderies should be the visible part of the imgage when clipped.
Using this custom clipper
#override
Rect getClip(Size size) {
Rect rect = Rect.fromLTRB(25,0,size.width - 25, size.height);
return rect;
}
#override
bool shouldReclip(CustomRect oldClipper) {
return true;
}
}
results in 25 px blank space left of the clipped image and 25 px blank space right of the clipped image.
And at least we ant to copy specific areas of an image and scale/position it exactly in the app... -> more complex desired result:
You need to add a transform to translate the widget over to the newly empty space. Just be aware that the widget itself will still occupy the same width in this example - so if there is some sibling in a row, for example, it will still get "pushed over" by the same amount of space. If you need to change that you'll need to add a SizedBox to the final part of this example so that you can trim down the size of the widget to the portion you've clipped.
Also note that this is not a very good practice - ideally you should be fetching the image you actually want to display. Flutter will still need to load your entire image into memory and then do some non-trivial work to add the clip you want. That takes up plenty of extra CPU and memory. But sometimes you don't have a choice I guess.
This example shows just displaying an image, followed by applying a custom clip, followed by applying a translation, which is what OP is looking for.
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart';
void main() {
final Widget image = Image.network(
'https://via.placeholder.com/300x60?text=This is just a placeholder');
const double widthAmount = 100;
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: <Widget>[
Spacer(),
image,
Spacer(),
ClipRect(
clipper: CustomRect(widthAmount),
child: image,
),
Spacer(),
Transform(
transform: Matrix4.translation(Vector3(-widthAmount, 0.0, 0.0)),
child: ClipRect(
clipper: CustomRect(widthAmount),
child: image,
),
),
Spacer(),
],
),
),
),
));
}
class CustomRect extends CustomClipper<Rect> {
CustomRect(this.widthAmount);
final double widthAmount;
#override
Rect getClip(Size size) {
Rect rect =
Rect.fromLTRB(widthAmount, 0, size.width - widthAmount, size.height);
return rect;
}
#override
bool shouldReclip(CustomRect oldClipper) {
return oldClipper.widthAmount != widthAmount;
}
}
Okay, the solution above is not really working as if we translate the clipped image we get some free space (the amount that we translate).
Isn't there any option in Flutter to just define an area of an widget and get this area as a new Widget/Image with exactly the width and height of the defined area to be able to position it (without any paddings/white space around),to scale it, ...?