'How to transform a widget by switching view?

I want a smooth animation that switch between the previous Scale and position to current Scale and postion. But it look like that it's not the exact previous scale or position. Why the only first widget have a reverse scale => ZoomIn instead of ZoomOut. I add a isSet variable because of the first frame that show the widget far away from where it should start. I have try with AnimatedSlide but it's not the same way to calculate. I don't think I should add more code but if it's necessary for you I would. Am I far from the right way to to it? I'll be grateful. Thank you.

enter image description here

class ItemTransform extends ConsumerStatefulWidget {
  final Widget child;
  final String idItem;

  const ItemTransform({required this.idItem, required this.child, Key? key})
      : super(key: key);

  @override
  _ItemTransformState createState() => _ItemTransformState();
}

class _ItemTransformState extends ConsumerState<ItemTransform>
    with SingleTickerProviderStateMixin {
  late bool isSet;
  late AnimationController _animationController;
  late Animation<double> _animationOffset;
  late Animation<double> _animationScale;

  @override
  void initState() {
    super.initState();
    isSet = false;
    _animationController = AnimationController(
        duration: const Duration(milliseconds: 500), vsync: this);

    _animationOffset = Tween<double>(begin: 1.0, end: 0.0).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));

    _animationScale = Tween(begin: 1.0, end: 1.0).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));

    _animationController.forward();

    WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
      _afterLayout();
    });
  }

  void _afterLayout() {
    if (!mounted) {
      return;
    }

    final RenderBox renderBoxRed = context.findRenderObject() as RenderBox;

    final size = renderBoxRed.size;
    ref.read(viewAnimationProvider).setSize({widget.idItem: size});

    final position = renderBoxRed.localToGlobal(Offset.zero);
    ref.read(viewAnimationProvider).setPosition({widget.idItem: position});

    isSet = true;
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _animationController,
        builder: (context, _) {
          final oldOffset =
              ref.read(viewAnimationProvider).oldPositions[widget.idItem];
          final newOffset =
              ref.read(viewAnimationProvider).newPositions[widget.idItem];
          Offset? toMove;
          if (newOffset != null && oldOffset != null) {
            if (isSet) {
              toMove = Offset(
                  (oldOffset.dx - newOffset.dx) * _animationOffset.value,
                  (oldOffset.dy - newOffset.dy) * _animationOffset.value);
            } else {
              toMove = Offset(
                  (newOffset.dx - oldOffset.dx) * _animationOffset.value,
                  (newOffset.dy - oldOffset.dy) * _animationOffset.value);
            }
          }

          final oldSizes =
              ref.read(viewAnimationProvider).oldSizes[widget.idItem];
          final newSizes =
              ref.read(viewAnimationProvider).newSizes[widget.idItem];

          if (oldSizes != null && newSizes != null) {
            final oldSize = oldSizes.width / oldSizes.height;
            final newSize = newSizes.width / newSizes.height;

            if (isSet ) {
              _animationScale = Tween(begin: newSize / oldSize, end: 1.0)
                  .animate(CurvedAnimation(
                      parent: _animationController, curve: Curves.easeInOut));
            }else{
              _animationScale = Tween(begin: oldSize / newSize, end: 1.0).animate(
                  CurvedAnimation(
                      parent: _animationController, curve: Curves.easeInOut));
            }
          }

          return Transform.translate(
            offset: toMove ?? Offset.zero,
            child: Transform.scale(
              scale: _animationScale.value,
              child: widget.child,
            ),
          );
        });
  }
}

final viewAnimationProvider =
    ChangeNotifierProvider<ViewAnimationController>((ref) {
  return ViewAnimationController();
});

class ViewAnimationController extends ChangeNotifier {
  late TypeOfView oldView;
  late TypeOfView newView;
  late Map<String, Offset> oldPositions;
  late Map<String, Offset> newPositions;
  late Map<String, Size> oldSizes;
  late Map<String, Size> newSizes;

  ViewAnimationController() {
    oldView = TypeOfView.staggered;
    newView = TypeOfView.normal;
    oldPositions = <String, Offset>{};
    newPositions = <String, Offset>{};
    oldSizes = <String, Size>{};
    newSizes = <String, Size>{};
  }

  void setView(TypeOfView newV) {
    oldView = newView;
    newView = newV;
    notifyListeners();
  }

  void setPosition(Map<String, Offset> position) {

    final newDx = position.values.first.dx;
    final newDy = position.values.first.dy;

    final offset = newPositions[position.keys.first];

    if (offset != null) {
      if (newDx != newPositions[position.keys.first]!.dx ||
          newDy != newPositions[position.keys.first]!.dy) {
        oldPositions[position.keys.first] = newPositions[position.keys.first]!;
        newPositions[position.keys.first] = position.values.first;
        // newPositions.addAll(position);
      }
    } else {
      newPositions.addAll(position);
    }
  }

  void setSize(Map<String, Size> map) {
    final width = map.values.first.width;
    final height = map.values.first.height;

    final size = newSizes[map.keys.first];

    if (size != null) {
      if (width != size.width || height != size.height) {
        oldSizes[map.keys.first] = newSizes[map.keys.first]!;
        newSizes[map.keys.first] = map.values.first;
      }
    } else {
      newSizes.addAll(map);
    }
  }
}

switch (ref.watch(currentViewProvider)) {
                                  case TypeOfView.normal:
                                    return GridViewCount(
                                        currentSize: currentSize,
                                        listItem: listItem as List<Item>);
                                  case TypeOfView.staggered:
                                    return GridViewStaggered(
                                        currentSize: currentSize,
                                        listItem: listItem as List<Item>);
                                }


Solution 1:[1]

It looks like you want a GridView that can be zoomed?

I've made a widget in the past, called "GalleryView", to achieve a similar kind of effect, but on mobile devices with "pinch-to-zoom" gesture. Let me put it here and see if it's helpful for you. Maybe you can use it as a starting point and modify it towards your end goal.

demo

Usage:

GalleryView.builder(
  itemCount: 101, // total items
  maxPerRow: 10, // the maximum zoom-out level allowed
  itemBuilder: (_, index) => MyImage(),
)

Source:

class GalleryView extends StatefulWidget {
  final int? itemCount;
  final IndexedWidgetBuilder itemBuilder;

  final int initialPerRow;
  final int minPerRow;
  final int maxPerRow;

  final Duration duration;
  final ScrollController? controller;

  const GalleryView.builder({
    Key? key,
    this.itemCount,
    required this.itemBuilder,
    this.initialPerRow = 3,
    this.minPerRow = 1,
    this.maxPerRow = 7,
    this.duration = const Duration(seconds: 1),
    this.controller,
  }) : super(key: key);

  @override
  _GalleryViewState createState() => _GalleryViewState();
}

class _GalleryViewState extends State<GalleryView> {
  late final ScrollController _controller =
      widget.controller ?? ScrollController();

  double _maxWidth = 0.0;

  late double _size; // size of each grid item
  late double _prevSize;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        if (constraints.maxWidth != _maxWidth) {
          _maxWidth = constraints.maxWidth;
          _size = _maxWidth / widget.initialPerRow;
          WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
            _snapToGrid();
          });
        }
        return GestureDetector(
          child: _buildListView(),
          onScaleStart: (_) {
            _controller.jumpTo(0);
            _prevSize = _size;
          },
          onScaleUpdate: (details) {
            final maxSize = _maxWidth / widget.minPerRow;
            final minSize = _maxWidth / widget.maxPerRow;
            setState(() {
              _size = (_prevSize * details.scale).clamp(minSize, maxSize);
            });
          },
          onScaleEnd: (_) => _snapToGrid(),
        );
      },
    );
  }

  _snapToGrid() {
    final countPerRow = (_maxWidth / _size).round().clamp(
          widget.minPerRow,
          widget.maxPerRow,
        );
    setState(() => _size = _maxWidth / countPerRow);
  }

  ListView _buildListView() {
    final countPerRow = (_maxWidth / _size).ceil();

    return ListView.builder(
      controller: _controller,
      itemExtent: _size,
      itemCount: widget.itemCount != null
          ? (widget.itemCount! / countPerRow).ceil()
          : null,
      itemBuilder: (context, int i) {
        return OverflowBox(
          maxWidth: double.infinity,
          alignment: Alignment.centerLeft,
          child: Row(
            children: [
              for (int j = 0; j < countPerRow; j++)
                if (widget.itemCount == null ||
                    i * countPerRow + j < widget.itemCount!)
                  _buildItem(context, i * countPerRow + j),
            ],
          ),
        );
      },
    );
  }

  Widget _buildItem(context, int index) {
    return SizedBox(
      width: _size,
      height: _size,
      child: AnimatedSwitcher(
        duration: widget.duration,
        child: KeyedSubtree(
          key: ValueKey(index),
          child: widget.itemBuilder(context, index),
        ),
      ),
    );
  }
}

Solution 2:[2]

enter image description hereI finally found a solution. I don't know if it's the best solution but it work as attended. I understand many things in my search. The "_animationScale" work badly because the with and height are same so I have to use "_animationHeight" and "_animationWidth". And I tried in a simple SizedBox and FittedBox to apply the size change but nothing change. Concerning the offset I use the topLeft with the translate and minus to the new coordonate. It's the same resulte before but I didn't know about that translate methode. I've tried to gather all that _animation in one animation like "Animation<Rect?>" like here or here, but it's relative to a fixe size if I really understand. If someone could clearly explain me why it work as attended? Is there another way with those two previous links? I'd like to give that bounty...

class ItemTransform extends ConsumerStatefulWidget {
  final Widget child;
  final String idItem;

  const ItemTransform({required this.idItem, required this.child, Key? key})
      : super(key: key);

  @override
  _ItemTransformState createState() => _ItemTransformState();
}

class _ItemTransformState extends ConsumerState<ItemTransform>
    with TickerProviderStateMixin {
  late bool isSet;
  late AnimationController _animationController;

  late Animation<Offset> _animationOffset;
  late Animation<double> _animationWidth;
  late Animation<double> _animationHeight;

  @override
  void initState() {
    super.initState();
    isSet = false;

    _animationController = AnimationController(
        duration: const Duration(milliseconds: 500), vsync: this);

    _animationOffset = Tween<Offset>(begin: Offset.zero, end: Offset.zero)
        .animate(CurvedAnimation(
            parent: _animationController, curve: Curves.easeInOut));

    _animationWidth = Tween(begin: 1.0, end: 1.0).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));

    _animationHeight = Tween(begin: 1.0, end: 1.0).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));

    WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
      _afterLayout();
    });
  }

  void _afterLayout() {
    if (!mounted) {
      return;
    }

    final RenderBox renderBox = context.findRenderObject() as RenderBox;

    final size = renderBox.size;

    _animationWidth = Tween(begin: size.width, end: size.width).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));

    _animationHeight = Tween(begin: size.height, end: size.height).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));

    final position = renderBox.localToGlobal(Offset.zero);

    ref.read(viewAnimationProvider).setNewRect(
        id: widget.idItem,
        newFromWidget:
            Rect.fromLTWH(position.dx, position.dy, size.width, size.height));

    if (ref.read(viewAnimationProvider).newRect[widget.idItem] != null) {
      _animationController.forward();
    }
    isSet = true;
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _animationController,
        builder: (context, _) {
          final oldRect =
              ref.read(viewAnimationProvider).oldRect[widget.idItem];
          final newRect =
              ref.read(viewAnimationProvider).newRect[widget.idItem];

          if (oldRect != null && newRect != null) {
            _animationOffset = Tween<Offset>(
                    begin: oldRect.topLeft
                        .translate(-newRect.topLeft.dx, -newRect.topLeft.dy),
                    end: Offset.zero)
                .animate(CurvedAnimation(
                    parent: _animationController, curve: Curves.easeInOut));
            //
            _animationWidth =
                Tween(begin: oldRect.size.width, end: newRect.size.width)
                    .animate(CurvedAnimation(
                        parent: _animationController, curve: Curves.easeInOut));
            _animationHeight =
                Tween(begin: oldRect.size.height, end: newRect.size.height)
                    .animate(CurvedAnimation(
                        parent: _animationController, curve: Curves.easeInOut));
          }

          return Offstage(
            offstage: !isSet,
            child: Transform.translate(
              offset: _animationOffset.value,
              child: Stack(
                clipBehavior: Clip.none,
                children: [
                  Positioned(
                    height: _animationHeight.value,
                    width: _animationWidth.value,
                    child: widget.child,
                  ),
                ],
              ),
            ),
          );
        });
  }
}
final viewAnimationProvider =
    ChangeNotifierProvider<ViewAnimationController>((ref) {
  return ViewAnimationController();
});

class ViewAnimationController extends ChangeNotifier {
  late Map<String,Rect> oldRect;
  late Map<String,Rect> newRect;


  ViewAnimationController() {
    oldRect = <String, Rect>{};
    newRect = <String, Rect>{};
  }

  void setNewRect({required String id, required Rect newFromWidget}){

    final rect = newRect[id];

    if(rect != null && rect != newFromWidget){
      oldRect[id] = rect;
      newRect[id] = newFromWidget;

    }else{
      newRect.addAll({id:newFromWidget});
    }

  }
}

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 WSBT
Solution 2