Why are my animations skipping steps sometimes? - flutter

I'm trying to animate a property of a CustomPainter widget that get's its values from an item provided by a Riverpod Notifier.
In this broken down example of my real app, I trigger the Notifier by changing the data of the second Item which then should resize the circle in front of the ListTile.
It seems to work for changes where the value increases but when the value decreases, it often jumps over parts of the animation.
I'm not sure if I'm doing the whole animation part right here.
The code is also on Dartpad:
https://dartpad.dev/?id=e3916b47603988efabd7a08712b98287
// ignore_for_file: avoid_print
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod + animated CustomPainter',
home: const Example3(),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.orange,
brightness: MediaQueryData.fromWindow(WidgetsBinding.instance.window).platformBrightness,
surface: Colors.deepOrange[600],
),
),
);
}
}
class ItemPainter extends CustomPainter {
final double value;
ItemPainter(this.value);
final itemPaint = Paint()..color = Colors.orange;
#override
void paint(Canvas canvas, Size size) {
// draw a circle with a size depending on the value
double radius = size.width / 10 * value / 2;
canvas.drawCircle(
Offset(
size.width / 2,
size.height / 2,
),
radius,
itemPaint,
);
}
#override
bool shouldRepaint(covariant ItemPainter oldDelegate) => oldDelegate.value != value;
}
CustomPaint itemIcon(double value) {
return CustomPaint(
painter: ItemPainter(value),
size: const Size(40, 40),
);
}
#immutable
class Item {
const Item({required this.id, required this.value});
final String id;
final double value;
}
// notifier that provides a list of items
class ItemsNotifier extends Notifier<List<Item>> {
#override
List<Item> build() {
return [
const Item(id: 'A', value: 1.0),
const Item(id: 'B', value: 5.0),
const Item(id: 'C', value: 10.0),
];
}
void randomize(String id) {
// replace the state with a new list of items where the value is randomized from 0.0 to 10.0
state = [
for (final item in state)
if (item.id == id) Item(id: item.id, value: Random().nextInt(100).toDouble() / 10.0) else item,
];
}
}
class AnimatedItem extends StatefulWidget {
final Item item;
const AnimatedItem(this.item, {super.key});
#override
State<AnimatedItem> createState() => _AnimatedItemState();
}
class _AnimatedItemState extends State<AnimatedItem> with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late Animation<double> animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
value: widget.item.value,
vsync: this,
duration: const Duration(milliseconds: 3000),
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
void didUpdateWidget(AnimatedItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.item.value != widget.item.value) {
print('didUpdateWidget: ${oldWidget.item.value} -> ${widget.item.value}');
_animationController.value = oldWidget.item.value / 10;
_animationController.animateTo(widget.item.value / 10);
}
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return itemIcon((widget.item.value * _animationController.value));
},
);
}
}
final itemsProvider = NotifierProvider<ItemsNotifier, List<Item>>(() => ItemsNotifier());
class Example3 extends ConsumerWidget {
const Example3({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(itemsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Animated CustomPainter Problem'),
),
// iterate over the item list in ItemsNotifier
body: ListView.separated(
separatorBuilder: (context, index) => const Divider(),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items.elementAt(index);
return ListTile(
key: Key(item.id),
leading: AnimatedItem(item),
title: Text('${item.value}'),
);
},
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
ref.read(itemsProvider.notifier).randomize('B'); // randomize the value of the second item
},
child: const Icon(Icons.change_circle),
),
],
),
);
}
}

Your issues lie completely in your implementation of the AnimationController. I don't really understand your intent with the original code, but the reason it jumped was because your were doing widget.item.value * _animationController.value in the build function. When you updated your item's value, it suddenly changed widget.item.value, creating the jump, then animating a small change with the AnimationController.
This code will work:
class _AnimatedItemState extends State<AnimatedItem> with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late Animation<double> animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
value: widget.item.value,
vsync: this,
duration: const Duration(milliseconds: 3000),
lowerBound: 0.0,
upperBound: 10.0
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
void didUpdateWidget(AnimatedItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.item.value != widget.item.value) {
print('didUpdateWidget: ${oldWidget.item.value} -> ${widget.item.value}');
_animationController.animateTo(widget.item.value);
}
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return itemIcon(_animationController.value);
},
);
}
}
This code adjusts the bounds of your AnimationController to accommodate the range of values you want to animate and only uses _animationController.value in build. I also removed a redundant line from didUpdateWidget, but that had no effect on functionality.

Related

Flutter How can I make this animation "smoother"

How can I make this animation "smoother" (currently is restarts harshly on the 5 second refresh). I'd like for it to reverse maybe from the last angle?
I call this widget as this:
SpinnerAnimation(body: Icon(FontAwesomeIcons.earthAmericas, size: 33, color: Colors.white,),),
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
class SpinnerAnimation extends StatefulWidget {
late final Widget body;
SpinnerAnimation({required this.body});
#override
_SpinnerAnimationState createState() =>
_SpinnerAnimationState(body: this.body);
}
class _SpinnerAnimationState extends State<SpinnerAnimation>
with SingleTickerProviderStateMixin {
late final Widget body;
_SpinnerAnimationState({required this.body});
late AnimationController _controller;
double change_rotation_speed = 1.2;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..repeat();
//timer repeat every 5 seconds:
Timer.periodic(Duration(seconds: 5), (Timer t) => change_rotation_speed = randoDoubleGeneratorWithRange(1.2, 5) );
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: body,
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _controller.value * change_rotation_speed * math.pi,
child: child,
);
},
);
}
double randoDoubleGeneratorWithRange(double min, double max) {
//return a random double value:
var random = new Random();
double result = min + random.nextDouble() * (max - min);
//print(result);
return result;
}
}
You can use RotationTransition and with Animation
.............
late AnimationController _controller;
late Animation<double> animationRotation;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
)..repeat(revere:true); // set true for reverse animation
animationRotation =
Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
}
........
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: body,
builder: (BuildContext context, Widget? child) {
return RotationTransition(
turns: animationRotation,
child: child,
);
},
);
here demo link on Dartpad: https://dartpad.dev/?id=e316af128311c0b42d7aef2fd9e037df

How to use BLoC pattern via flutter_bloc library?

I'm writting a small tamagotchi app using Flutter and now I'm learning how to use flutter_bloc lib.
When user tap on a pet image on a screen, it must redraw a CircularPercentIndicator widget, but it won't work. I'm trying to connect a view with a bloc using a BlocBuilder and BlocProvider classes, but it did not help.
After tapping a pet widget, animation is forwarded, but the state of saturationCount and CircularPercentIndicator hasn't been updated.
Here is my BLoC for pet feeding:
class PetFeedingBloc extends Bloc<SaturationEvent, SaturationState> {
PetFeedingBloc()
: super(const SaturationState(saturationCount: 40.0)) {
on<SaturationSmallIncrementEvent>((event, emit) => state.saturationCount + 15.0);
on<SaturationBigIncrementEvent>((event, emit) => state.saturationCount + 55.0);
on<SaturationDecrementEvent>((event, emit) => state.saturationCount - 2.0);
}
}
In SaturationBarWidget class I'm trying to connect a percent indicator in a widget with a BLoC, but it does not work. Here it is:
class SaturationBarWidget extends StatefulWidget {
const SaturationBarWidget({Key? key}) : super(key: key);
#override
State<SaturationBarWidget> createState() => SaturationBarWidgetState();
}
class SaturationBarWidgetState extends State<SaturationBarWidget> {
#override
void initState() {
Timer? timer;
timer = Timer.periodic(const Duration(milliseconds: 3000), (_) {
setState(() {
context.read<PetFeedingBloc>().add(SaturationDecrementEvent());
if (context.read<PetFeedingBloc>().state.saturationCount <= 0) {
timer?.cancel();
}
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<PetFeedingBloc, SaturationState>(builder: (context, state){
return CircularPercentIndicator(
radius: 50.0,
lineWidth: 20.0,
animateFromLastPercent: true,
percent: context.read<PetFeedingBloc>().state.saturationCount / 100,
center: const Icon(
Icons.emoji_emotions_outlined,
size: 50.0,
),
backgroundColor: Colors.blueGrey,
progressColor: Colors.blue,
);
});
}
}
And here it is my PetWidget class with image that need to be tapped:
class PetWidget extends StatefulWidget {
const PetWidget({Key? key}) : super(key: key);
#override
State<PetWidget> createState() => PetWidgetState();
}
class PetWidgetState extends State<PetWidget> with TickerProviderStateMixin {
late Animation<Offset> _animation;
late AnimationController _animationController;
static GlobalKey<SaturationBarWidgetState> key = GlobalKey();
bool reverse = true;
Image cat = Image.asset('images/cat.png');
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 4));
_animation = Tween<Offset>(begin: Offset.zero, end: const Offset(1, 0))
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.elasticIn));
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reverse();
}
});
//_animationController.forward();
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Center(child:
BlocBuilder<PetFeedingBloc, SaturationState>(builder: (context, state) {
return Center(
child: SizedBox(
width: 300,
height: 400,
child: SlideTransition(
position: _animation,
child: GestureDetector(
child: cat,
onDoubleTap: () {
context.read<PetFeedingBloc>().add(SaturationBigIncrementEvent());
_animationController.forward();
},
onTap: () {
context.read<PetFeedingBloc>().add(SaturationSmallIncrementEvent());
_animationController.forward();
},
),
),
)
);
})
);
}
}
I think you have to call the emit method in you
PetFeedingBloc
class PetFeedingBloc extends Bloc<SaturationEvent, SaturationState> {
PetFeedingBloc()
: super(const SaturationState(saturationCount: 40.0)) {
on<SaturationSmallIncrementEvent>((event, emit) => emit(SaturationState(saturationCount: state.saturationCount + 15.0)) );
...
}
}

RepaintBoundary with a StreamBuilder

I thought I understood RepaintBoundary but now I don't.
Background
I wrote this answer describing how you can add a RepaintBoundary around a widget that has to draw a lot to prevent other parts of the widget tree from redrawing. That worked as expected.
Problem now
I'm trying to make a real life example now where the widget is being rebuilt inside a StreamBuilder based on an audio player stream. I tried wrapping the whole StreamBuilder in a RepaintBoundary like this:
#override
Widget build(BuildContext context) {
print("building app");
return Scaffold(
body: Column(
children: [
Spacer(),
RepaintBoundary(
child: ProgressBarWidget(
durationState: _durationState, player: _player),
),
RepaintBoundary(
child: PlayPauseButton(player: _player),
),
],
),
);
}
But the rest of the UI is still repainting (except the play/pause button which I also wrapped in a RepaintBoundary).
The build method of that ProgressBarWidget looks like this:
#override
Widget build(BuildContext context) {
print('building progress bar');
return StreamBuilder<DurationState>(
stream: _durationState,
builder: (context, snapshot) {
final durationState = snapshot.data;
final progress = durationState?.progress ?? Duration.zero;
final buffered = durationState?.buffered ?? Duration.zero;
final total = durationState?.total ?? Duration.zero;
return ProgressBar(
progress: progress,
buffered: buffered,
total: total,
onSeek: (duration) {
_player.seek(duration);
},
);
},
);
}
But if I remove the StreamBuilder like this:
#override
Widget build(BuildContext context) {
print('building progress bar');
return ProgressBar(
progress: Duration.zero,
total: Duration(minutes: 5),
onSeek: (duration) {
_player.seek(duration);
},
);
}
Then the repaint boundary works again when I manually move the thumb.
What is it about the StreamBuilder that makes the RepaintBoundary not work?
Full code
The full code for the widget layout is here:
import 'package:flutter/material.dart';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/rendering.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
void main() {
debugRepaintTextRainbowEnabled = true;
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
home: HomeWidget(),
);
}
}
class HomeWidget extends StatefulWidget {
#override
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
AudioPlayer _player;
final url = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3';
Stream<DurationState> _durationState;
#override
void initState() {
super.initState();
_player = AudioPlayer();
_durationState = Rx.combineLatest2<Duration, PlaybackEvent, DurationState>(
_player.positionStream,
_player.playbackEventStream,
(position, playbackEvent) => DurationState(
progress: position,
buffered: playbackEvent.bufferedPosition,
total: playbackEvent.duration,
));
_init();
}
Future<void> _init() async {
try {
await _player.setUrl(url);
} catch (e) {
print("An error occured $e");
}
}
#override
Widget build(BuildContext context) {
print("building app");
return Scaffold(
body: Column(
children: [
Spacer(),
RepaintBoundary(
child: ProgressBarWidget(
durationState: _durationState, player: _player),
),
RepaintBoundary(
child: PlayPauseButton(player: _player),
),
],
),
);
}
}
class ProgressBarWidget extends StatelessWidget {
const ProgressBarWidget({
Key key,
#required Stream<DurationState> durationState,
#required AudioPlayer player,
}) : _durationState = durationState,
_player = player,
super(key: key);
final Stream<DurationState> _durationState;
final AudioPlayer _player;
#override
Widget build(BuildContext context) {
print('building progress bar');
return StreamBuilder<DurationState>(
stream: _durationState,
builder: (context, snapshot) {
final durationState = snapshot.data;
final progress = durationState?.progress ?? Duration.zero;
final buffered = durationState?.buffered ?? Duration.zero;
final total = durationState?.total ?? Duration.zero;
return ProgressBar(
progress: progress,
buffered: buffered,
total: total,
onSeek: (duration) {
_player.seek(duration);
},
);
},
);
// ProgressBar(
// progress: Duration.zero,
// total: Duration(minutes: 5),
// onSeek: (duration) {
// _player.seek(duration);
// },
// );
}
}
class PlayPauseButton extends StatelessWidget {
const PlayPauseButton({
Key key,
#required AudioPlayer player,
}) : _player = player,
super(key: key);
final AudioPlayer _player;
#override
Widget build(BuildContext context) {
print('building play/pause button');
return StreamBuilder<PlayerState>(
stream: _player.playerStateStream,
builder: (context, snapshot) {
final playerState = snapshot.data;
final processingState = playerState?.processingState;
final playing = playerState?.playing;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
return Container(
margin: EdgeInsets.all(8.0),
width: 64.0,
height: 64.0,
child: CircularProgressIndicator(),
);
} else if (playing != true) {
return IconButton(
icon: Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: _player.play,
);
} else if (processingState != ProcessingState.completed) {
return IconButton(
icon: Icon(Icons.pause),
iconSize: 64.0,
onPressed: _player.pause,
);
} else {
return IconButton(
icon: Icon(Icons.replay),
iconSize: 64.0,
onPressed: () => _player.seek(Duration.zero),
);
}
},
);
}
}
class DurationState {
const DurationState({this.progress, this.buffered, this.total});
final Duration progress;
final Duration buffered;
final Duration total;
}
The whole project is on GitHub.
When you don't have the StreamBuilder and drag in the ProgressBar, it will probably just repaint itself and not require a relayout.
When the StreamBuilder gets a new event from the stream, it rebuilds ProgressBar. Depending on the details of ProgressBar, when it gets rebuild it will also require a relayout (perhaps it contains a layout builder). Since it is in a Column and the Column uses the size of it children during layout (to determine the position of the next child), then Column has to do it layout again as well, which might cause its children to need a repaint.
Play around with this: You'll notice that marking Foo to repaint (horizontal drag) only causes Foo to repaint (when it is wrapped with a RepaintBoundary). Marking Foo for relayout (a tap) will also cause the Column to relayout and repaint. When the LayoutBuilder is present (which causes a relayout when it is rebuild), you'll see that a rebuild of Foo (by vertical drag) also causes the Column to repaint.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => Column(
children: [
Container(
height: 400,
color: Color(0x11ff0000),
),
RepaintBoundary(
child: Foo(),
),
],
);
}
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
#override
Widget build(BuildContext context) => GestureDetector(
onHorizontalDragUpdate: (_) => context.findRenderObject().markNeedsPaint(),
onTap: () => context.findRenderObject().markNeedsLayout(),
onVerticalDragUpdate: (_) => setState(() {}),
child: LayoutBuilder(
builder: (context, _) => Container(
height: 100,
width: 100.0,
color: Color(0xff002200),
),
),
);
}
This is a supplemental answer to tell how specifically I solved the problem after getting #spkersten's help.
The ProgressBar widget was rebuilding internally whenever the text labels would change. My first attempt at solving the problem was to wrap the widget in a SizedBox with a fixed height and width. This did work in that it prevented the rest of the screen from needing relayout or repainting. However, it was difficult to know what the height of the progress bar was going to be before laying it out.
So my second solution was to paint the text manually rather than use Text widgets. That way I could refrain from calling markNeedsLayout when the text changed. This solved the problem.
My current implementation of the progress bar is here.

Flutter InteractiveViewer onInteractionEnd return to scale of 1.0

I'd like to have the image I am scaling return to the original scale value(1.0) when it is released.
onInteractionEnd seems like the right property to do this with but I am not sure how to access a scale property to create a function that does this.
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(0.0),
minScale: 1.0,
maxScale: 2.5,
onInteractionEnd: //scale = 1.0,
You can copy paste run full code below
This is modification of official example of transformationController https://api.flutter.dev/flutter/widgets/InteractiveViewer/transformationController.html
Whenever the child is transformed, the Matrix4 value is updated and all listeners are notified. If the value is set, InteractiveViewer will update to respect the new value.
You can in onInteractionEnd reset animation as official demo do
code snippet
void _animateResetInitialize() {
_controllerReset.reset();
_animationReset = Matrix4Tween(
begin: _transformationController.value,
end: Matrix4.identity(),
).animate(_controllerReset);
_animationReset.addListener(_onAnimateReset);
_controllerReset.forward();
}
void _onInteractionEnd(ScaleEndDetails details) {
_animateResetInitialize();
}
working demo
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
/// AnimationControllers can be created with `vsync: this` because of TickerProviderStateMixin.
class _MyStatefulWidgetState extends State<MyStatefulWidget>
with TickerProviderStateMixin {
final TransformationController _transformationController =
TransformationController();
Animation<Matrix4> _animationReset;
AnimationController _controllerReset;
void _onAnimateReset() {
_transformationController.value = _animationReset.value;
if (!_controllerReset.isAnimating) {
_animationReset?.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
}
}
void _animateResetInitialize() {
_controllerReset.reset();
_animationReset = Matrix4Tween(
begin: _transformationController.value,
end: Matrix4.identity(),
).animate(_controllerReset);
_animationReset.addListener(_onAnimateReset);
_controllerReset.forward();
}
// Stop a running reset to home transform animation.
void _animateResetStop() {
_controllerReset.stop();
_animationReset?.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
}
void _onInteractionStart(ScaleStartDetails details) {
// If the user tries to cause a transformation while the reset animation is
// running, cancel the reset animation.
if (_controllerReset.status == AnimationStatus.forward) {
_animateResetStop();
}
}
void _onInteractionEnd(ScaleEndDetails details) {
_animateResetInitialize();
}
#override
void initState() {
super.initState();
_controllerReset = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
}
#override
void dispose() {
_controllerReset.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Controller demo'),
),
body: Center(
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(double.infinity),
transformationController: _transformationController,
minScale: 1.0,
maxScale: 2.5,
onInteractionStart: _onInteractionStart,
onInteractionEnd: _onInteractionEnd,
child: Image.network("https://picsum.photos/250?image=9")
),
),
persistentFooterButtons: [
IconButton(
onPressed: _animateResetInitialize,
tooltip: 'Reset',
color: Theme.of(context).colorScheme.surface,
icon: const Icon(Icons.replay),
),
],
);
}
}
I came up with a simple solution.
Just save the initial controller value on the start of the interaction, then change back to that value when the interaction ends.
TransformationController controllerT = TransformationController();
var initialControllerValue;
InteractiveViewer(
minScale: 1.0,
maxScale: 100.0,
transformationController: controllerT,
onInteractionStart: (details){
initialControllerValue = controllerT.value;
},
onInteractionEnd: (details){
controllerT.value = initialControllerValue;
},
...
If you want this in a grid or list view, just declare the controller in the itemBuilder:
ListView.builder(
itemCount: widget.types.length,
itemBuilder: (context, index) {
TransformationController controllerT = TransformationController();
var initialControllerValue;

In Flutter is it possible to increase the transparency of a Dismissible widget the further it is dismissed?

I have a Dismissible widget in my application that I drag down to dismiss. There is a requirement that the transparency of the Dismissible should increase the further it is dragged down. So it should look as if it is fading out as it is dismissed. If it were to be dragged back up, its transparency should decrease.
As a simple test I tried wrapping the Dismissible in a Listener and Opacity widget. The opacity value is set to a variable tracked in state. The Listener widget listens to the total "y" axis movement of the Dismissible and when it reaches a certain threshold, decreases the the opacity value tracked in state. See code below for example:
import 'package:flutter/material.dart';
class FadingDismissible extends StatefulWidget {
#override
_FadingDismissible createState() => _FadingDismissible();
}
class _FadingDismissible extends State<FadingDismissible> {
double _totalMovement = 0;
double _opacity;
#override
void initState() {
super.initState();
_opacity = 1.0;
}
_setOpacity(double opacityValue) {
setState(() {
_opacity = opacityValue;
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerMove: (PointerMoveEvent event) {
_totalMovement += event.delta.dy;
if (_totalMovement > 200) {
_setOpacity(0.5);
}
},
onPointerUp: (PointerUpEvent event) {
_setOpacity(1.0);
_totalMovement = 0;
},
child: Opacity(
opacity: _opacity,
child: Dismissible(
direction: DismissDirection.down,
key: UniqueKey(),
onDismissed: (direction) {
Navigator.pop(context);
},
child: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
),
body: Container(color: Colors.blue),
),
),
),
);
}
}
The issue is, whenever the state is set, the widget is re-built and the Dismissible jumps back to the top.
Right now I'm not sure of another way around this. Is there a way to change the transparency of a Dismissible widget as it is dragged? Or will I have to use a different widget altogether?
Thanks!
I think might be the closest if you do not want to create your own Dismissible widget:
class FadingDismissible extends StatefulWidget {
final String text;
FadingDismissible({#required this.text});
#override
_FadingDismissibleState createState() => _FadingDismissibleState();
}
class _FadingDismissibleState extends State<FadingDismissible> {
double opacity = 1.0;
StreamController<double> controller = StreamController<double>();
Stream stream;
double startPosition;
#override
void initState() {
super.initState();
stream = controller.stream;
}
#override
void dispose() {
super.dispose();
controller.close();
}
#override
Widget build(BuildContext context) {
return Dismissible(
key: GlobalKey(),
child: StreamBuilder(
stream: stream,
initialData: 1.0,
builder: (context, snapshot) {
return Listener(
child: Opacity(
opacity: snapshot.data,
child: Text(widget.text),
),
onPointerDown: (event) {
startPosition = event.position.dx;
},
onPointerUp: (event) {
opacity = 1.0;
controller.add(opacity);
},
onPointerMove: (details) {
if (details.position.dx > startPosition) {
var move = details.position.dx - startPosition;
move = move / MediaQuery.of(context).size.width;
opacity = 1 - move;
controller.add(opacity);
}
},
);
},
),
);
}
}
Here is another method, similar to the one posted by #Sneider but uses a ValueNotifier and ValueListenableBuilder instead of Stream and `StreamBuilder.
import 'package:flutter/material.dart';
class FadingDismissible extends StatefulWidget {
const FadingDismissible({Key? key}) : super(key: key);
#override
State<FadingDismissible> createState() => _FadingDismissibleState();
}
class _FadingDismissibleState extends State<FadingDismissible> {
final ValueNotifier<double> _opacity = ValueNotifier(1.0);
late double _startPosition;
#override
Widget build(BuildContext context) {
return Dismissible(
key: UniqueKey(),
child: Listener(
onPointerDown: (event) {
_startPosition = event.position.dx;
},
onPointerUp: (event) {
_opacity.value = 1.0;
},
onPointerMove: (event) {
if (event.position.dx < _startPosition) {
// Dismiss Left
var move = _startPosition - event.position.dx;
move = move / MediaQuery.of(context).size.width;
_opacity.value = 1.0 - move;
} else {
// Dismiss Right
var move = event.position.dx - _startPosition;
move = move / MediaQuery.of(context).size.width;
_opacity.value = 1.0 - move;
}
},
child: ValueListenableBuilder(
valueListenable: _opacity,
builder: (BuildContext context, double opacity, Widget? child) {
return Opacity(
opacity: opacity > 0.2 ? opacity : 0.2,
child: Container(
color: Colors.red,
width: 100,
height: 100,
),
);
},
),
),
);
}
}