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);
}
}
Related
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 am building a custom TextSelectionControls with an extends from MaterialTextSelectionControls.
How do I verticalize the Text Selection Toolbar in TextField?
It's like this now:
And I want it to be like this so I can add more custom options:
This is the part that builds the toolbar:
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
children: [
...itemDatas
.asMap()
.entries
.map((MapEntry<int, _TextSelectionToolbarItemData> entry) {
return TextSelectionToolbarTextButton(
padding: TextSelectionToolbarTextButton.getPadding(
entry.key, itemDatas.length),
onPressed: entry.value.onPressed,
child: Text(entry.value.label),
);
}),
],
);
And this is the complete code:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class CustomTextSelectionControls extends MaterialTextSelectionControls {
/// Builder for material-style copy/paste text selection toolbar.
#override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset selectionMidpoint,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ClipboardStatusNotifier? clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) {
return _TextSelectionControlsToolbar(
globalEditableRegion: globalEditableRegion,
textLineHeight: textLineHeight,
selectionMidpoint: selectionMidpoint,
endpoints: endpoints,
delegate: delegate,
clipboardStatus: clipboardStatus,
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
handleSelectAll:
canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
);
}
}
class _TextSelectionControlsToolbar extends StatefulWidget {
const _TextSelectionControlsToolbar({
required this.clipboardStatus,
required this.delegate,
required this.endpoints,
required this.globalEditableRegion,
required this.handleCut,
required this.handleCopy,
required this.handlePaste,
required this.handleSelectAll,
required this.selectionMidpoint,
required this.textLineHeight,
});
final ClipboardStatusNotifier? clipboardStatus;
final TextSelectionDelegate delegate;
final List<TextSelectionPoint> endpoints;
final Rect globalEditableRegion;
final VoidCallback? handleCut;
final VoidCallback? handleCopy;
final VoidCallback? handlePaste;
final VoidCallback? handleSelectAll;
final Offset selectionMidpoint;
final double textLineHeight;
#override
_TextSelectionControlsToolbarState createState() =>
_TextSelectionControlsToolbarState();
}
class _TextSelectionControlsToolbarState
extends State<_TextSelectionControlsToolbar> with TickerProviderStateMixin {
// Padding between the toolbar and the anchor.
static const double _kToolbarContentDistanceBelow = 20.0;
static const double _kToolbarContentDistance = 8.0;
void _onChangedClipboardStatus() {
setState(() {
// Inform the widget that the value of clipboardStatus has changed.
});
}
#override
void initState() {
super.initState();
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
}
#override
void didUpdateWidget(_TextSelectionControlsToolbar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.clipboardStatus != oldWidget.clipboardStatus) {
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
}
}
#override
void dispose() {
super.dispose();
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
}
#override
Widget build(BuildContext context) {
// If there are no buttons to be shown, don't render anything.
if (widget.handleCut == null &&
widget.handleCopy == null &&
widget.handlePaste == null &&
widget.handleSelectAll == null) {
return const SizedBox.shrink();
}
// If the paste button is desired, don't render anything until the state of
// the clipboard is known, since it's used to determine if paste is shown.
if (widget.handlePaste != null &&
widget.clipboardStatus?.value == ClipboardStatus.unknown) {
return const SizedBox.shrink();
}
// Calculate the positioning of the menu. It is placed above the selection
// if there is enough room, or otherwise below.
final TextSelectionPoint startTextSelectionPoint = widget.endpoints[0];
final TextSelectionPoint endTextSelectionPoint =
widget.endpoints.length > 1 ? widget.endpoints[1] : widget.endpoints[0];
final Offset anchorAbove = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
widget.globalEditableRegion.top +
startTextSelectionPoint.point.dy -
widget.textLineHeight -
_kToolbarContentDistance,
);
final Offset anchorBelow = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
widget.globalEditableRegion.top +
endTextSelectionPoint.point.dy +
_kToolbarContentDistanceBelow,
);
// Determine which buttons will appear so that the order and total number is
// known. A button's position in the menu can slightly affect its
// appearance.
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final List<_TextSelectionToolbarItemData> itemDatas =
<_TextSelectionToolbarItemData>[
if (widget.handleCut != null)
_TextSelectionToolbarItemData(
label: localizations.cutButtonLabel,
onPressed: widget.handleCut!,
),
if (widget.handleCopy != null)
_TextSelectionToolbarItemData(
label: localizations.copyButtonLabel,
onPressed: widget.handleCopy!,
),
if (widget.handlePaste != null &&
widget.clipboardStatus?.value == ClipboardStatus.pasteable)
_TextSelectionToolbarItemData(
label: localizations.pasteButtonLabel,
onPressed: widget.handlePaste!,
),
if (widget.handleSelectAll != null)
_TextSelectionToolbarItemData(
label: localizations.selectAllButtonLabel,
onPressed: widget.handleSelectAll!,
),
];
// If there is no option available, build an empty widget.
if (itemDatas.isEmpty) {
return const SizedBox(width: 0.0, height: 0.0);
}
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
children: [
...itemDatas
.asMap()
.entries
.map((MapEntry<int, _TextSelectionToolbarItemData> entry) {
return TextSelectionToolbarTextButton(
padding: TextSelectionToolbarTextButton.getPadding(
entry.key, itemDatas.length),
onPressed: entry.value.onPressed,
child: Text(entry.value.label),
);
}),
],
);
}
}
class _TextSelectionToolbarItemData {
const _TextSelectionToolbarItemData({
required this.label,
required this.onPressed,
});
final String label;
final VoidCallback onPressed;
}
I was able to customize the SelectionToolbar using the toolbarBuilder. You may encounter an error in which case you should use the widget used in the Material widget:
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
toolbarBuilder: (context, _) => Material(
child: SizedBox(
width: 230,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(child: ListTile())
],
),
),
),
children: const [SizedBox.shrink()],
);
Customizing the TextSelectionToolbar is the best solution I found:
import 'package:flutter/material.dart';
class CostumeSelectionToolbar extends TextSelectionToolbar {
const CostumeSelectionToolbar({
super.key,
required super.anchorAbove,
required super.anchorBelow,
required super.children,
});
static const double _kToolbarScreenPadding = 8.0;
static const double _kToolbarHeight = 275.0;
#override
Widget build(BuildContext context) {
final double paddingAbove =
MediaQuery.of(context).padding.top + _kToolbarScreenPadding;
final double availableHeight = anchorAbove.dy - paddingAbove;
final bool fitsAbove = _kToolbarHeight <= availableHeight;
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
return Padding(
padding: EdgeInsets.fromLTRB(
_kToolbarScreenPadding,
paddingAbove,
_kToolbarScreenPadding,
_kToolbarScreenPadding,
),
child: Stack(
children: <Widget>[
CustomSingleChildLayout(
delegate: TextSelectionToolbarLayoutDelegate(
anchorAbove: anchorAbove - localAdjustment,
anchorBelow: anchorBelow - localAdjustment,
fitsAbove: fitsAbove,
),
child: SizedBox(
width: 230,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
),
],
),
);
}
}
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 set any widget to screen at any position so if user open app in other devices the widget must should remain at that give position , for example if I set widget on 1/4 part of the screen then it should be the same in every device . I tested in simulator from Iphone 8 to Ipad 8th gen.
Below is my code I am using Positioned.fromRect and passing Offset(dx,dy). I also tried MediaQuery but all in vain . Please guide me even if you find my question stupid.
Reference app Story click or post maker app with pre built in templates.
This is simple constructor.
FlutterSimpleStickerImage(
this.image, {
Key key,
this.width,
this.height,
this.viewport,
this.minScale = 1.0,
this.maxScale = 2.0,
this.onTapRemove,
}) : super(key: key);
Widget image;
final double width;
final double height;
final Size viewport;
final double minScale;
final double maxScale;
A class for positioning widget in stack the Position.fromRect()
class _FlutterSimpleStickerImageState extends State<FlutterSimpleStickerImage> {
_FlutterSimpleStickerImageState();
double _scale = 1.0;
double _previousScale = 1.0;
Offset _previousOffset = Offset(0, 0);
Offset _startingFocalPoint = Offset(0, 0);
Offset _offset = Offset(0, 0);
double _rotation = 0.0;
double _previousRotation = 0.0;
bool _isSelected = false;
#override
void dispose() {
super.dispose();
_offset = Offset(0, 0);
_scale = 1.0;
}
#override
Widget build(BuildContext context) {
double sizewidth=183;
double sizeheight=0;
return Positioned.fromRect(
rect: Rect.fromPoints(Offset(sizewidth, sizeheight),Offset(sizewidth , sizeheight )),
child: Container(
child: Stack(
children: <Widget>[
Center(
child: Transform(
transform: Matrix4.diagonal3(Vector3(_scale, _scale, _scale)),
// ..setRotationZ(_rotation),
alignment: FractionalOffset.center,
child: GestureDetector(
onScaleStart: (ScaleStartDetails details) {
_startingFocalPoint = Offset(sizewidth,sizeheight);
_previousOffset = Offset(sizewidth,sizeheight);
_previousRotation = _rotation;
_previousScale = _scale;
// print(
// "begin - focal : ${details.focalPoint}, local : ${details.localFocalPoint}");
},
onScaleUpdate: (ScaleUpdateDetails details) {
_scale = min(
max(_previousScale * details.scale, widget.minScale),
widget.maxScale);
_rotation = details.rotation;
final Offset normalizedOffset =
(_startingFocalPoint - _previousOffset) /
_previousScale;
Offset __offset =
details.focalPoint - (normalizedOffset * _scale);
__offset = Offset(max(__offset.dx, -widget.width),
max(__offset.dy, -widget.height));
__offset = Offset(min(__offset.dx, widget.viewport.width),
min(__offset.dy, widget.viewport.height));
setState(() {
_offset = __offset;
// print("move - $_offset, scale : $_scale");
});
},
onTap: () {
setState(() {
_isSelected = !_isSelected;
});
},
onTapCancel: () {
setState(() {
_isSelected = false;
});
},
onDoubleTap: () {
setState(() {
_scale = 1.0;
});
},
onLongPress: (){
setState(() {
widget.image=Container(height: 100,width: 100,color: Color.fromRGBO(255, 150, 0, 1.0),);
});
},
child: Container(child: widget.image),
),
),
),
_isSelected
? Positioned(
top: 12,
right: 12,
width: 24,
height: 24,
child: Container(
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.remove_circle),
color: Color.fromRGBO(255, 150, 0, 1.0),
onPressed: () {
print('tapped remove sticker');
if (this.widget.onTapRemove != null) {
this.widget.onTapRemove(this.widget);
}
},
),
),
)
: Container(),
],
),
),
);
}
void hideRemoveButton() {
setState(() {
_isSelected = false;
});
}
}