'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!



Solution 1:[1]

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);
              }
            },
          );
        },
      ),
    );
  }
}

Solution 2:[2]

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,
              ),
            );
          },
        ),
      ),
    );
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Snieder
Solution 2 Chris