I am trying to custom my container shape to look like this:
I tried to do it with customPaint but I don't know very well this widget so I need help.
How can I draw a shape like this? Is customPaint the right solution?
You can copy paste run full code below
modified code of package https://pub.dev/packages/flutter_custom_clippers 's
StarClipper https://github.com/lohanidamodar/flutter_custom_clippers/blob/master/lib/src/star_clipper.dart
code snippet
class StarClipper extends CustomClipper<Path>
#override
Path getClip(Size size) {
...
double radius = halfWidth / 1.3;
...
Container(
height: 200,
width: 200,
child: ClipPath(
clipper: StarClipper(14),
child: Container(
height: 150,
color: Colors.green[500],
child: Center(child: Text("+6", style: TextStyle(fontSize: 50),)),
),
),
),
working demo
full code
import 'package:flutter/material.dart';
import 'dart:math' as math;
class StarClipper extends CustomClipper<Path> {
StarClipper(this.numberOfPoints);
/// The number of points of the star
final int numberOfPoints;
#override
Path getClip(Size size) {
double width = size.width;
print(width);
double halfWidth = width / 2;
double bigRadius = halfWidth;
double radius = halfWidth / 1.3;
double degreesPerStep = _degToRad(360 / numberOfPoints);
double halfDegreesPerStep = degreesPerStep / 2;
var path = Path();
double max = 2 * math.pi;
path.moveTo(width, halfWidth);
for (double step = 0; step < max; step += degreesPerStep) {
path.lineTo(halfWidth + bigRadius * math.cos(step),
halfWidth + bigRadius * math.sin(step));
path.lineTo(halfWidth + radius * math.cos(step + halfDegreesPerStep),
halfWidth + radius * math.sin(step + halfDegreesPerStep));
}
path.close();
return path;
}
num _degToRad(num deg) => deg * (math.pi / 180.0);
#override
bool shouldReclip(CustomClipper<Path> oldClipper) {
StarClipper oldie = oldClipper as StarClipper;
return numberOfPoints != oldie.numberOfPoints;
}
}
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: 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> {
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>[
Container(
height: 200,
width: 200,
child: ClipPath(
clipper: StarClipper(14),
child: Container(
height: 150,
color: Colors.green[500],
child: Center(child: Text("+6", style: TextStyle(fontSize: 50),)),
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
I was trying to create Turkish flag for fun. Creating crescent was easy but creating star is not look easy. so I just tried to use my geometric and trigonometric knowledge to draw but no success. while continuing searching I encountered this link . Yeess I found what I look for. of course it was about different topic but functions was useful. so I reach my goal at the end of night.
So creating custom stars are possible with CustomPainter since now. Because I m sleepy and tired of being awake whole night I share all of code block with no further explanation. You can adjust offsetts according to your need. if any question. I m ready to answer. Enjoy coding.
class TurkishFlagPaint extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
// x and y coordinates of starting point
final bx = 95;
final by = 0;
final paint = Paint();
paint.color = Colors.white;
final innerCirclePoints = 5; //how many edges you need?
final innerRadius = 80 / innerCirclePoints;
final innerOuterRadiusRatio = 2.5;
final outerRadius = innerRadius * innerOuterRadiusRatio;
List<Map> points =
calcStarPoints(bx, by, innerCirclePoints, innerRadius, outerRadius);
var star = Path()..moveTo(points[0]['x'], points[0]['y']);
points.forEach((point) {
star.lineTo(point['x'], point['y']);
});
canvas.drawPath(
Path.combine(
PathOperation.union,
//this combine for crescent
Path.combine(
PathOperation.difference,
Path()..addOval(Rect.fromCircle(center: Offset(-20, 0), radius: 80)),
Path()
..addOval(Rect.fromCircle(center: Offset(2, 0), radius: 60))
..close(),
),
star,// I also combine cresscent with star
),
paint,
);
}
//This function is life saver.
//it produces points for star edges inner and outer. if you need to //rotation of star edges.
// just play with - 0.3 value in currX and currY equations.
List<Map> calcStarPoints(
centerX, centerY, innerCirclePoints, innerRadius, outerRadius) {
final angle = ((math.pi) / innerCirclePoints);
var angleOffsetToCenterStar = 0;
var totalPoints = innerCirclePoints * 2; // 10 in a 5-points star
List<Map> points = [];
for (int i = 0; i < totalPoints; i++) {
bool isEvenIndex = i % 2 == 0;
var r = isEvenIndex ? outerRadius : innerRadius;
var currY =
centerY + math.cos(i * angle + angleOffsetToCenterStar - 0.3) * r;
var currX =
centerX + math.sin(i * angle + angleOffsetToCenterStar - 0.3) * r;
points.add({'x': currX, 'y': currY});
}
return points;
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
now you can use painter in a widget;
Center(
child: CustomPaint(
painter: TurkishFlagPaint(),
),
),
and the result will be like this :
Yes, CustomPaint is the right solution. You can calculate the Path (series of points around the Container) and then paint it with the drawPath method of the canvas.
For example, the path of a triangle would look like this:
return Path()
..moveTo(0, y)
..lineTo(x / 2, 0)
..lineTo(x, y)
..lineTo(0, y);
The Path starts at (0,y) (top-left), then a line to (x/2,0) (bottom-center) gets added and so on. This snipped was taken from this answer.
This code will help you build a centered star, to use it you just have to instantiate it in a ClipPath
import 'package:flutter/material.dart';
import 'dart:math' as math;
const STAR_POINTS = 5;
class StarClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var centerX = size.width / 2;
var centerY = size.height / 2;
var path = Path();
var radius = size.width / 2;
var inner = radius / 2;
var rotation = math.pi / 2 * 3;
var step = math.pi / STAR_POINTS;
path.lineTo(centerX, centerY - radius);
for (var i = 0; i < STAR_POINTS; i++) {
var x = centerX + math.cos(rotation) * radius;
var y = centerY + math.sin(rotation) * radius;
path.lineTo(x, y);
rotation += step;
x = centerX + math.cos(rotation) * inner;
y = centerY + math.sin(rotation) * inner;
path.lineTo(x, y);
rotation += step;
}
path.lineTo(centerX, centerY - radius);
path.close();
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
/// Instance
ClipPath(
clipper: StarClipper(),
child: Container(
color: Colors.red,
width: 80,
height: 80,
),
)
Related
Unable to achieve the page to start from the bottom in the page viewer I have tried converted bit of code this but was unable to load it from bottom.
Reference link to what I am trying to achieve : Swipe effect like inshorts news app
Here is my code for what I am trying
class InshortsPageTransformer extends PageTransformer {
#override
Widget transform(Widget child, TransformInfo info) {
double position = info.position ?? 0;
double rotation = position * 0.2;
double scale = math.max(0.8, 1 - position.abs());
if (position < -1) {
rotation = 0;
child = Opacity(
opacity: 0,
child: child,
);
} else if (position <= 0) {
child = Opacity(
opacity: 1,
child: child,
);
child = Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(rotation)
..scale(scale),
alignment: Alignment.center,
child: child,
);
} else if (position <= 1) {
child = Opacity(
opacity: 1,
child: child,
);
child = Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(rotation)
..scale(scale),
alignment: Alignment.center,
child: child,
);
} else {
rotation = 0;
child = Opacity(
opacity: 0,
child: child,
);
}
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(rotation)
..scale(scale),
alignment: Alignment.center,
child: child,
);
}
}
class ScaleAndFadeTransformer extends PageTransformer {
final double _scale;
final double _fade;
ScaleAndFadeTransformer({double fade = 0.3, double scale = 0.8})
: _fade = fade,
_scale = scale;
#override
Widget transform(Widget item, TransformInfo info) {
double position = info.position ?? 0;
double scaleFactor = (1 - position.abs()) * (1 - _scale);
double fadeFactor = (1 - position.abs()) * (1 - _fade);
double opacity = _fade + fadeFactor;
double scale = _scale + scaleFactor;
return Opacity(
opacity: opacity,
child: Transform.scale(
scale: scale,
child: item,
),
);
}
}
I recently created the same project look into it
https://github.com/bibek-ranjan-saha/prodt_test
I have used https://pub.dev/packages/another_transformer_page_view this package for achieving this effect.
sample code snippet
TransformerPageView(
scrollDirection: Axis.vertical,
itemCount: snapshot.data?.data.length ?? 0,
loop: false,
controller: homePageController,
transformer: DeepthPageTransformer()
itemBuilder: (context, index) {
return NewsView(
screenSize: screenSize,
news: snapshot.data!.data[index],
);
},
);
I am developing an shape editor in flutter. For that, I need to add a feature of adding shapes dynamically to the screen. Each of them should be able to independently scale, position, and rotate. There should be an anchor point like in word or in Photoshop to indicate the active shapes and to scale and rotate them. The shape should be positioned with drag and drop.
I found this answer :
Rotate and resize container
But the answer only rotate the shape from the center, I want to scale the shape from the corners also.
The code :
class FooResizer extends StatefulWidget {
#override
_FooResizerState createState() => _FooResizerState();
}
class _FooResizerState extends State<FooResizer> with TickerProviderStateMixin, ChangeNotifier {
Sizer currentSizer;
double angle = 0.0;
AnimationController ctrl;
final Map<String, Sizer> sizers = {
'l': Sizer('l', Alignment.centerLeft, {'t': 0.5, 'b': 0.5, 'M': 0.5}),
't': Sizer('t', Alignment.topCenter, {'l': 0.5, 'r': 0.5, 'M': 0.5}),
'r': Sizer('r', Alignment.centerRight, {'t': 0.5, 'b': 0.5, 'R': 1.0, 'M': 0.5}),
'b': Sizer('b', Alignment.bottomCenter, {'l': 0.5, 'r': 0.5, 'R': 1.0, 'M': 0.5}),
'R': Sizer('R', Alignment.bottomRight, {}),
'M': Sizer('M', Alignment.center, {}),
};
#override
void initState() {
super.initState();
ctrl = AnimationController(vsync: this, duration: Duration(milliseconds: 300), value: 1.0);
}
#override
Widget build(BuildContext context) {
return ClipRect(
child: ColoredBox(
color: Colors.black12,
// CustomMultiChildLayoutPainter:
// https://gist.github.com/pskink/0f82724b41d9ebe89604782fbf62fe03#file-multi_layout_painter-dart-L447
child: CustomMultiChildLayoutPainter(
delegate: _FooResizerDelegate(sizers, this),
children: [
LayoutId(
id: 'body',
child: AnimatedBuilder(
animation: this,
builder: (ctx, child) {
return Transform.rotate(
angle: angle,
child: child,
);
},
child: Material(color: Colors.grey[300], elevation: 4, child: FlutterLogo()),
),
),
...sizers.values.map(_sizerBuilder),
],
),
),
);
}
Widget _sizerBuilder(Sizer sizer) {
final colors = {
'M': Colors.orange,
'R': Colors.teal,
};
return LayoutId(
id: sizer.id,
child: GestureDetector(
onPanStart: (details) => _panStart(sizer),
onPanUpdate: _panUpdate,
onPanEnd: _panEnd,
child: AnimatedBuilder(
animation: ctrl,
builder: (context, child) {
final color = colors[sizer.id] ?? Colors.green;
return Opacity(
opacity: currentSizer == sizer? 1.0 : Curves.ease.transform(ctrl.value),
child: Container(
decoration: ShapeDecoration(
shape: CircleBorder(side: BorderSide(width: lerpDouble(0.0, 2.0, ctrl.value), color: Colors.black38)),
color: currentSizer == sizer? Color.lerp(Colors.deepPurple, color, ctrl.value) : color,
shadows: [BoxShadow.lerp(BoxShadow(spreadRadius: 2, blurRadius: 4, offset: Offset(2, 2)), null, ctrl.value)],
),
),
);
}
),
),
);
}
_panStart(Sizer sizer) {
currentSizer = sizer;
ctrl.reverse();
}
_panUpdate(DragUpdateDetails details) {
assert(currentSizer != null);
if (currentSizer.id == 'M') {
// move
sizers.values.forEach((sizer) => sizer.center += details.delta);
} else
if (currentSizer.id == 'R') {
// rotate
final localCenter = sizers['M'].center;
final globalCenter = (context.findRenderObject() as RenderBox).localToGlobal(localCenter);
final angle0 = (details.globalPosition - details.delta - globalCenter).direction;
final angle1 = (details.globalPosition - globalCenter).direction;
final deltaAngle = angle1 - angle0;
sizers.values
.where((sizer) => sizer.id != 'M')
.forEach((sizer) {
final vector = sizer.center - localCenter;
sizer.center = localCenter + Offset.fromDirection(vector.direction + deltaAngle, vector.distance);
});
angle += deltaAngle;
} else {
// resize
final adjustedAngle = angle + currentSizer.angleAdjustment;
final rotatedDistance = details.delta.distance * math.cos(details.delta.direction - adjustedAngle);
final vector = Offset.fromDirection(adjustedAngle, rotatedDistance);
currentSizer.center += vector;
currentSizer.dependents.forEach((id, factor) => sizers[id].center += vector * factor);
}
notifyListeners();
}
_panEnd(DragEndDetails details) {
assert(currentSizer != null);
// currentSizer = null;
ctrl.forward();
}
}
class _FooResizerDelegate extends MultiChildLayoutPainterDelegate {
static const SIZE = 48.0;
final Map<String, Sizer> sizers;
_FooResizerDelegate(this.sizers, Listenable relayout) : super(relayout: relayout);
#override
void performLayout(Size size) {
sizers['M'].center ??= init(size);
for (var sizer in sizers.values) {
layoutChild(sizer.id, BoxConstraints.tight(Size(SIZE, SIZE)));
positionChild(sizer.id, sizer.center - Offset(SIZE / 2, SIZE / 2));
}
final w = (sizers['l'].center - sizers['r'].center).distance;
final h = (sizers['t'].center - sizers['b'].center).distance;
layoutChild('body', BoxConstraints.tight(Size(w, h)));
positionChild('body', sizers['M'].center - Offset(w / 2, h / 2));
}
Offset init(Size size) {
final rect = (Offset.zero & size).deflate(24);
print('init rect: $rect');
for (var sizer in sizers.values) {
sizer
..center = sizer.alignment.withinRect(rect)
..angleAdjustment = sizer.alignment.x == 0? math.pi / 2 : 0;
}
return sizers['M'].center;
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
#override
void foregroundPaint(Canvas canvas, Size size) {
}
#override
void paint(Canvas canvas, Size size) {
final w = (sizers['r'].center - sizers['l'].center).distance;
final h = (sizers['b'].center - sizers['t'].center).distance;
final rect = Rect.fromCenter(center: sizers['M'].center, width: w, height: h);
final angle = (sizers['r'].center - sizers['l'].center).direction;
final matrix = Matrix4.identity()
..translate(rect.center.dx, rect.center.dy)
..rotateZ(angle)
..translate(-rect.center.dx, -rect.center.dy);
final transformedRect = MatrixUtils.transformRect(matrix, rect);
final points = [
Offset(transformedRect.left, 0), Offset(transformedRect.left, size.height),
Offset(0, transformedRect.top), Offset(size.width, transformedRect.top),
Offset(transformedRect.right, 0), Offset(transformedRect.right, size.height),
Offset(0, transformedRect.bottom), Offset(size.width, transformedRect.bottom),
];
canvas.drawPoints(PointMode.lines, points, Paint());
}
}
class Sizer {
final String id;
final Alignment alignment;
final Map<String, double> dependents;
Offset center;
double angleAdjustment;
Sizer(this.id, this.alignment, this.dependents);
}
The result :
what I am trying to achieve :
I want to create an infinite rain of icons in Flutter. Let me explain.
Here is the widget that will be performing the raining/falling animation. It uses an AnimationController along with a GravitySimulation to make the widget fall. It takes in an endDistance, which is how far it should go down. It renders a simple icon (also an argument).
import 'package:clickr/providers/GameEngineProvider.dart';
import 'package:clickr/utils/functions/randInt.dart';
import "package:flutter/material.dart";
import 'package:flutter/physics.dart';
import 'package:provider/provider.dart';
class FallingIcon extends StatefulWidget {
final double endDistance;
final double? iconLeft;
final double? iconRight;
final double? iconBottom;
final Widget? child;
const FallingIcon({
Key? key,
required this.endDistance,
this.iconLeft,
this.iconRight,
this.iconBottom,
this.child,
}) : super(key: key);
#override
_FallingIconState createState() => _FallingIconState();
}
class _FallingIconState extends State<FallingIcon>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(
milliseconds: randInt(2000, 3000),
),
);
_controller.animateWith(
GravitySimulation(
10,
0,
widget.endDistance,
0,
),
);
_controller.forward();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Positioned(
child: child!,
top: _controller.value * widget.endDistance,
left: widget.iconLeft,
right: widget.iconRight,
bottom: widget.iconBottom,
);
},
child: widget.child ?? const FlutterLogo(),
);
}
}
Here, I am using this widget inside a stack:
import 'package:clickr/providers/GameEngineProvider.dart';
import 'package:clickr/providers/IconSetsProvider.dart';
import 'package:clickr/screens/HomeScreen.Authenticated.dart';
import 'package:clickr/widgets/CircleSticker.dart';
import 'package:clickr/widgets/FallingIcon.dart';
import 'package:clickr/widgets/StatusBarSpacer.dart';
import "package:flutter/material.dart";
import 'package:provider/provider.dart';
import '../widgets/FloatingActionButtonCustomBar.dart';
class PlayScreen extends StatefulWidget {
const PlayScreen({Key? key}) : super(key: key);
static const routeName = "/play";
#override
State<PlayScreen> createState() => _PlayScreenState();
}
class _PlayScreenState extends State<PlayScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
final iconSetsProvider =
Provider.of<IconSetsProvider>(context, listen: false);
final gameEngineProvider =
Provider.of<GameEngineProvider>(context, listen: true);
return Scaffold(
body: Column(
children: [
const StatusBarSpacer(),
Expanded(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Stack(
children: [
FallingIcon(
endDistance: constraints.maxHeight,
child: Image.asset(
iconSetsProvider.getRandomIconFromIconSet(
iconSetsProvider.currentIconSet!,
),
width: 40,
),
),
],
);
},
),
),
FloatingActionButtonCustomBar(
children: [
FloatingActionButton(
onPressed: () {
Navigator.of(context).pushReplacementNamed(
HomeScreenAuthenticatated.routeName);
},
child: const Icon(Icons.home),
heroTag: "home",
),
FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.pause),
heroTag: "pause",
),
],
),
],
),
);
}
}
The above code renders one FallingIcon, which successfully animates and falls until it reaches the end of the screen and cannot be seen any more. Now, I want to multiply this icon, so there should be multiple icons falling on the screen. And I want this to never stop. It should just be an infinite rain of icons. And it shouldn't be systematic, like one wave after the other. It should just be random infinite rain of icons.
I have attempted to use a stream and a new event would be pushed onto the stream randomly. But then I couldn't figure out how to render FallingIcons based on the items in the stream.
I wonder if this is possible in Flutter. Thanks.
this kind of "raining/falling animation" could be implemented with one CustomPaint widget (instead of rebuilding lots of widgets with AnimatedBuilder)
the core idea is to provide a Listenable variable to CustomPainter.repaint property (see super(repaint: ...) below) - of course you need to change your CustomPainter so that it draws your vertically falling images
class FooSpritePaint extends StatefulWidget {
#override
State<FooSpritePaint> createState() => _FooSpritePaintState();
}
class _FooSpritePaintState extends State<FooSpritePaint> with TickerProviderStateMixin {
late Ticker ticker;
final notifier = ValueNotifier(Duration.zero);
ui.Image? sprite;
#override
void initState() {
super.initState();
ticker = Ticker(_tick);
rootBundle.load('images/sprites.png')
.then((data) => decodeImageFromList(data.buffer.asUint8List()))
.then(_setSprite);
}
_tick(Duration d) => notifier.value = d;
_setSprite(ui.Image image) {
setState(() {
// print('image: $image');
sprite = image;
ticker.start();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
foregroundPainter: SpritePainter(sprite, notifier),
child: Center(
child: TextButton(
onPressed: () => ticker.isTicking? ticker.stop() : ticker.start(),
child: const Text('click to stop / start', textScaleFactor: 1.5),
),
),
),
);
}
}
class SpritePainter extends CustomPainter {
final ui.Image? sprite;
final ValueNotifier<Duration> notifier;
final p = Paint();
SpritePainter(this.sprite, this.notifier) : super(repaint: notifier);
#override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
if (sprite != null) {
final ms = notifier.value.inMilliseconds;
final frame = ms ~/ 80;
// print(frame);
final frames = [frame, frame + 2, frame + 4].map((f) => f % 6).toList();
final spritePhases = [
phase(size.width, ms * 0.066),
phase(size.width, ms * 0.075 + 40),
phase(size.width, ms * 0.075 + 80),
];
final transforms = [
for (int i = 0; i < spritePhases.length; i++)
ui.RSTransform(1, 0, spritePhases[i][0], size.height / 2 - 45),
];
final rects = [
for (int i = 0; i < spritePhases.length; i++)
Rect.fromLTWH(frames[i] * 100, spritePhases[i][1] * 100, 100, 100),
];
canvas.drawAtlas(sprite!, transforms, rects, null, null, null, p);
}
}
List<double> phase(double width, double x) {
final w = width + 100;
x = x % (2 * w);
return x < w? [x - 100, 0] : [2 * w - x - 100, 1];
}
#override
bool shouldRepaint(SpritePainter oldDelegate) => false;
}
the above sample code uses the following image that should be placed in images/sprites.png folder:
EDIT
and as a proof of concept here you have such a sample CustomPainter:
class Bird {
Bird(int ms, this.rect, List<double> r, Size size) :
startTimeMs = ms,
scale = lerpDouble(1, 0.3, r[0])!,
rotation = pi * lerpDouble(-1, 1, r[2])!,
xSimulation = FrictionSimulation(0.75, r[1] * size.width, lerpDouble(size.width / 2, -size.width / 2, r[1])!),
ySimulation = GravitySimulation(lerpDouble(10, 1000, r[0])!, -rect.height / 2, size.height + rect.height / 2, 100);
final int startTimeMs;
final Rect rect;
final Simulation xSimulation;
final Simulation ySimulation;
final double scale;
final double rotation;
double x(int ms) => xSimulation.x(_normalizeTime(ms));
double y(int ms) => ySimulation.x(_normalizeTime(ms));
bool isDead(int ms) => ySimulation.isDone(_normalizeTime(ms));
double _normalizeTime(int ms) => (ms - startTimeMs) / Duration.millisecondsPerSecond;
RSTransform transform(int ms, Size size) {
final translateY = y(ms);
return RSTransform.fromComponents(
translateX: x(ms),
translateY: translateY,
anchorX: rect.width / 2,
anchorY: rect.height / 2,
rotation: rotation * translateY / size.height,
scale: scale,
);
}
}
class FallingBirdsPainter extends CustomPainter {
final ui.Image? sprite;
final ValueNotifier<Duration> notifier;
final imagePaint = Paint();
final backgroundPaint = Paint()..color = Colors.black26;
final random = Random();
final birds = <Bird>[];
int nextReport = 0;
static const spriteRects = [
Rect.fromLTRB(000, 0, 103, 140),
Rect.fromLTRB(103, 0, 217, 140),
Rect.fromLTRB(217, 0, 312, 140),
Rect.fromLTRB(312, 0, 410, 140),
];
FallingBirdsPainter(this.sprite, this.notifier) : super(repaint: notifier);
#override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
canvas.drawPaint(backgroundPaint);
if (sprite != null) {
final ms = DateTime.now().millisecondsSinceEpoch;
if (random.nextDouble() < 0.15) {
// drop new bird
birds.add(Bird(ms, spriteRects[random.nextInt(4)], List.generate(3, (i) => random.nextDouble()), size));
}
final transforms = birds.map((bird) => bird.transform(ms, size)).toList();
final rects = birds.map((bird) => bird.rect).toList();
canvas.drawAtlas(sprite!, transforms, rects, null, null, null, imagePaint);
// dead birds cleanup
birds.removeWhere((bird) => bird.isDead(ms));
if (ms >= nextReport) {
nextReport = ms + 6000;
print('flying birds population: ${birds.length}');
}
}
}
#override
bool shouldRepaint(FallingBirdsPainter oldDelegate) => false;
}
it uses the following image:
all you need is to replace
foregroundPainter: SpritePainter(sprite, notifier),
with
foregroundPainter: FallingBirdsPainter(sprite, notifier),
and
rootBundle.load('images/sprites.png')
with
rootBundle.load('images/birds.png')
the final result is similar to this:
I want to add multiple sliders in one slider. If you're not clear about what I'm asking please refer the below image
I want these three squares to be sliding and get the values of them.
I did some searching and could not find any flutter widget or a plugin that has the support.
I tried to use a stack and use multiple Slider widgets at the same location but it is also not working. (I know it's not a good approach.)
How can I make this happen. To have multiple sliders on the same line and get the values.
Any help or ideas are very much appreciated.
Using Stack with three sliders did not work because it was being overlapped.
I have made this Slider3X of being curious. There are few things need to fix here, start and end points missing some fractional position.
Code on Gist, dart pad
class Slider3x extends StatefulWidget {
const Slider3x({
Key? key,
required this.onSliderUpdate,
this.size = const Size(5, 10),
this.min = 0,
this.max = 1.0,
this.colorX = Colors.green,
this.colorY = Colors.blue,
this.colorZ = Colors.redAccent,
}) : super(key: key);
final Function(double? x, double? y, double? z) onSliderUpdate;
///size of moveable 3x point 😅, forgot the name maybe thumbs
final Size size;
final double? min;
final double? max;
final Color colorX;
final Color colorY;
final Color colorZ;
#override
State<Slider3x> createState() => _Slider3xState();
}
class _Slider3xState extends State<Slider3x> {
/// three slider position
double? x;
double? y;
double? z;
final double tapSpacesArea = .05;
// currect active slider , help to prevent overlLAp while sliding
int activeSliderNumber = 0;
//* Update sldier
void _updateSlider(double dx, double maxWidth) {
final tapPosition = dx;
//* update logic
if (tapPosition <= 0 || tapPosition >= maxWidth) {
return;
}
//* update on UI based on slider number
if (activeSliderNumber == 0) {
setState(() {
x = tapPosition;
});
} else if (activeSliderNumber == 1) {
setState(() {
y = tapPosition;
});
} else if (activeSliderNumber == 2) {
setState(() {
z = tapPosition;
});
}
//pass value on main widget
widget.onSliderUpdate(
dp(_generateSliderValue(maxWidth: maxWidth, x: x!)),
dp(_generateSliderValue(maxWidth: maxWidth, x: y!)),
dp(_generateSliderValue(maxWidth: maxWidth, x: z!)),
);
}
//round number
double dp(double val, {int places = 2}) {
num mod = pow(10.0, places);
return ((val * mod).round().toDouble() / mod);
}
//* calculate slider value
double _generateSliderValue({
required double maxWidth,
required double x,
}) {
// x is slider original position on width:maxWidth
return (widget.max! - widget.min!) * (x / maxWidth) + widget.min!;
}
//* select ActiveSlider, fixed overLap issue
//* slider Selector logic
void _selectSlider({
required double maxWidth,
required double tapPosition,
}) {
final maxArea = maxWidth * tapSpacesArea;
if ((tapPosition - x!).abs() < maxArea) {
setState(() {
activeSliderNumber = 0;
});
} else if ((tapPosition - y!).abs() < maxArea) {
setState(() {
activeSliderNumber = 1;
});
} else if ((tapPosition - z!).abs() < maxArea) {
setState(() {
activeSliderNumber = 2;
});
}
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 50,
child: LayoutBuilder(builder: (context, constraints) {
final maxWidth = constraints.maxWidth - 10;
x = x ?? 0;
y = y ?? constraints.maxWidth / 2;
z = z ?? maxWidth;
return Stack(
alignment: Alignment.center,
children: [
Positioned(
left: x,
child: Container(
height: activeSliderNumber == 0
? widget.size.height * 1.5
: widget.size.height,
width: widget.size.width,
color: widget.colorX,
),
),
//* paint Y
Positioned(
left: y,
child: Container(
height: activeSliderNumber == 1
? widget.size.height * 1.5
: widget.size.height,
width: widget.size.width,
color: widget.colorY,
),
),
//* paint z
Positioned(
left: z,
child: Container(
height: activeSliderNumber == 2
? widget.size.height * 1.5
: widget.size.height,
width: widget.size.width,
color: widget.colorZ,
),
),
const Divider(
endIndent: 10,
),
GestureDetector(
onTapDown: (details) => _selectSlider(
maxWidth: maxWidth,
tapPosition: details.localPosition.dx),
onPanUpdate: (details) =>
_updateSlider(details.localPosition.dx, maxWidth),
),
],
);
}),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.min.toString(),
),
Text(
widget.max.toString(),
),
],
)
],
),
);
}
}
I am trying to extract a value, touchedIndex, from a child widget PieChartWidget that I have to make a pie chart with a hole in the middle using fl_chart, into its parent widget Co2Tracker to allow me to create some text in the middle of the pie chart that changes when a segment of the pie chart is pressed. This returns an error but everything works visually other than the pie chart segments no longer expand.To make debugging easier I replaced the setState(){isTouched = isTouchedNew} with print(isTouchedNew) but this still returns the error below, however, the pie chart now expands.
PieChart example
When using a callback function
int isTouched = -1;
setIsTouched(int isTouchedNew) {
print(isTouchedNew);
}
integrated within a stateful widget I get an output of
════════ Exception caught by gesture ═══════════════════════════════════════════════════════════════
The method 'call' was called on null.
Receiver: null
Tried calling: call(-1)
parent main page
class Co2Tracker extends StatefulWidget {
#override
_Co2TrackerState createState() => _Co2TrackerState();
}
class _Co2TrackerState extends State<Co2Tracker> {
List<String> pieNames = ['Travel', 'Gas', 'Electricity', 'Food shop', 'Water',];
int isTouched = -1;
double radiusNormal = 60;
double centerRadius = 50;
double radiusExpansion = 1.2;
setIsTouched(int isTouchedNew) {
print(isTouchedNew);
}
#override
Widget build(BuildContext context) {
//double co2 = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: appBarTemplate('CO\u2082 Tracker'),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: textStyleTemplateLarge(text: 'Your CO\u2082 emissions today', textColor: Colors.green[400]),
),
AspectRatio(
aspectRatio: 200/((radiusExpansion*radiusNormal) + centerRadius),
child: Row(
children: <Widget>[
Expanded(
child: Stack(
children: <Widget>[
Center(
child: FlatButton(child: textStyleTemplateLarge(text: '70kg', textColor: Colors.black),),
),
Center(
child: PieChartWidget(radiusNormal: radiusNormal, centerRadius: centerRadius, radiusExpanded: (radiusExpansion*radiusNormal), pieNames: pieNames,),
),
],
),
),
],
),
),
],
),
);
}
}
child pie chart widget
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
class PieChartWidget extends StatefulWidget {
final double radiusNormal;
final double radiusExpanded;
final double centerRadius;
final double fontSizeNormal;
final double fontSizeExpanded;
final List<String> pieNames;
final Function(int) setIsTouched;
PieChartWidget({this.radiusNormal, this.radiusExpanded, this.centerRadius, this.fontSizeNormal, this.fontSizeExpanded, this.pieNames, this.setIsTouched});
#override
_PieChartWidgetState createState() => _PieChartWidgetState(radiusNormal: radiusNormal, radiusExpanded: radiusExpanded, centerRadius: centerRadius,
fontSizeNormal: fontSizeNormal, fontSizeExpanded: fontSizeExpanded, pieNames: pieNames, setIsTouched: setIsTouched,);
}
class _PieChartWidgetState extends State<PieChartWidget> {
int touchedIndex = -1;
double radiusNormal;
double radiusExpanded;
double centerRadius;
double fontSizeNormal;
double fontSizeExpanded;
List<String> pieNames;
Function(int) setIsTouched;
_PieChartWidgetState({this.radiusNormal = 50, this.radiusExpanded = 60, this.centerRadius = 40, this.fontSizeNormal = 16, this.fontSizeExpanded = 25,
this.pieNames, this.setIsTouched,});
#override
Widget build(BuildContext context) {
return PieChart(
PieChartData(
sections: pieSections(pieNames),
borderData: FlBorderData(show: false),
sectionsSpace: 0,
centerSpaceRadius: centerRadius,
pieTouchData: PieTouchData(touchCallback: (pieTouchResponse) {
setState(() {
if (pieTouchResponse.touchInput is FlLongPressEnd ||
pieTouchResponse.touchInput is FlPanEnd) {
touchedIndex = -1;
} else {
touchedIndex = pieTouchResponse.touchedSectionIndex;
}
});
widget.setIsTouched(touchedIndex);
}),
),
);
}
-----------------I-think-everything-below-this-is-irrelevant-but-I-could-be-wrong-------------------------------------------
List<PieChartSectionData> pieSections(List<String> pieNames,) {
return List.generate(5, (i) {
final isTouched = i == touchedIndex;
final double fontSize = isTouched ? 25 : 16;
final double radius = isTouched ? radiusExpanded : radiusNormal;
final int colorNumber = (1+i) * 100;
final List<double> valueList = [20, 10, 10, 20, 30];
final double total = valueList.fold(0, (a, b) => a+b);
switch (i) {
case 0:
return sectionData(valueList[i], Colors.pink[300], title(isTouched, valueList[i], total, pieNames[i]), radius, fontSize);
case 1:
return sectionData(valueList[i], Colors.blue[600], title(isTouched, valueList[i], total, pieNames[i]), radius, fontSize);
case 2:
return sectionData(valueList[i], Colors.amber[300], title(isTouched, valueList[i], total, pieNames[i]), radius, fontSize);
case 3:
return sectionData(valueList[i], Colors.purple[300], title(isTouched, valueList[i], total, pieNames[i]), radius, fontSize);
case 4:
return sectionData(valueList[i], Colors.blue[300], title(isTouched, valueList[i], total, pieNames[i]), radius, fontSize);
default:
return null;
}
});
}
PieChartSectionData sectionData(double value, Color color, String title, double radius, double fontSize,){
return PieChartSectionData(
value: value, color: color, title: title, radius: radius,
titleStyle: TextStyle(color: Colors.black, fontSize: fontSize, fontWeight: FontWeight.bold),
);
}
String title(bool isTouched, double value, double total, String title,) {
return isTouched ? '${double.parse((value/total*100).toStringAsFixed(2))}%' : '$title';
}
}
My thoughts so far have been that maybe it is to do with initialization of variables or maybe this touchCallback function from the child page line 43 but no luck so far.
Thanks in advance for your time.
The problem is that when your printing an integer.
setIsTouched(int isTouchedNew) { print(isTouchedNew); }
You're supposed to print it like this
print("${isTouchedNew}")
Umm..not sure if you tried this,
int isTouched = -1;
setIsTouched(int isTouchedNew) {
setState(() {
isTouched=isTouchedNew;
print(isTouchedNew);
}
}