Flutter's TweenAnimationBuilder Tween animation reverts back when Tween is run again
Tags: flutter, animation
Published: 2023-05-28 21:23:39

Hi,

I am creating Match-3 game in flutter app, I have used TweenAnimationBuilder for exchanging two adjacent tiles spots.

The source code is as below:
```
import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';

class Tile extends StatelessWidget {
  List mBitmap = [
    Image.asset('assets/images/games/match-3/tile0.jpg'),
    Image.asset('assets/images/games/match-3/tile1.jpg'),
    Image.asset('assets/images/games/match-3/tile2.jpg'),
    Image.asset('assets/images/games/match-3/tile3.jpg'),
  ];

  var mBitmapDestroy = Image.asset('assets/images/games/match-3/heart.jpg');

  Image image = Image.asset("");

  late int value;
  late int row;
  late int col;

  Tile({required this.value, required this.row, required this.col, Key? key})
      : super(key: key);

  Image? getImage() {
    switch (value) {
      case 0:
        return mBitmap[0];
      case 1:
        return mBitmap[1];
      case 2:
        return mBitmap[2];
      case 3:
        return mBitmap[3];
      case 4:
        return mBitmapDestroy;
    }
  }

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

class TileMatchingGamePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const TileMatchingGameStatePage();
  }
}

class TileMatchingGameStatePage extends StatefulWidget {
  const TileMatchingGameStatePage({Key? key}) : super(key: key);

  @override
  State<TileMatchingGameStatePage> createState() =>
      _TileMatchingGameStatePage();
}

class _TileMatchingGameStatePage extends State<TileMatchingGameStatePage>
    with SingleTickerProviderStateMixin {
  List<List<int>> board = [
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
    [
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
      Random().nextInt(4),
    ],
  ];

  late Animation<Offset> _animation;
  late AnimationController _controller;
  Offset _offset = Offset.zero;

  List<List<Tile>> tiles = List.generate(
    7,
    (i) => List.generate(7, (j) => Tile(value: 0, row: i, col: j)),
  );

  late final mTileWidth;
  late final mTileHeight;

  @override
  void initState() {
    super.initState();
    mTileWidth = MediaQuery.of(context).size.width / 7;
    mTileHeight = mTileWidth;
    _controller =
        AnimationController(vsync: this, duration: Duration(milliseconds: 300));
    _animation = Tween<Offset>(begin: Offset.zero, end: Offset.zero)
        .animate(_controller);
    _animation.addListener(() {
      setState(() {
        // 현재 오프셋을 애니메이션 값으로 업데이트
        _offset = _animation.value;
      });
    });

    for (int i = 0; i < 7; i++) {
      for (int j = 0; j < 7; j++) {
        switch (board[i][j]) {
          case 0:
            tiles[i][j] = Tile(value: 0, row: i, col: j);
            break;
          case 1:
            tiles[i][j] = Tile(value: 1, row: i, col: j);
            break;
          case 2:
            tiles[i][j] = Tile(value: 2, row: i, col: j);
            break;
          case 3:
            tiles[i][j] = Tile(value: 3, row: i, col: j);
            break;
          case 4:
            tiles[i][j] = Tile(value: 4, row: i, col: j);
            break;
        }
      }
    }
  }

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

  void swapTiles(int i, int j, int k, int l) {
    setState(() {
      var temp = tiles[i][j];
      tiles[i][j] = tiles[k][l];
      tiles[k][l] = temp;
      var temp2 = board[i][j];
      board[i][j] = board[k][l];
      board[k][l] = temp2;
    });
  }

  // 드래그한 타일의 인덱스
  int? dragI;
  int? dragJ;

  // 인접한 타일의 인덱스
  int? swapI;
  int? swapJ;

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      shrinkWrap: true,
      itemCount: 7 * 7,
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 7,
      ),
      itemBuilder: (BuildContext context, int index) {
        int i = index ~/ 7;
        int j = index % 7;

        Offset offset = Offset.zero;
        if (i == dragI && j == dragJ) {
          // 드래그한 타일이면 인접한 타일 방향으로 이동
          if (swapI != null && swapJ != null) {
            offset = Offset(
              ((swapJ! - dragJ!) * mTileWidth).toDouble(),
              ((swapI! - dragI!) * mTileHeight).toDouble(),
            );
          }
        } else if (i == swapI && j == swapJ) {
          // 인접한 타일이면 드래그한 타일 방향으로 이동
          if (dragI != null && dragJ != null) {
            offset = Offset(
              ((dragJ! - swapJ!) * mTileWidth).toDouble(),
              ((dragI! - swapI!) * mTileHeight).toDouble(),
            );
          }
        }

        // 애니메이션 적용
        return TweenAnimationBuilder<Offset>(
          tween: Tween(begin: Offset.zero, end: offset),
          duration: Duration(milliseconds: 300),
          builder: (context, value, child) {
            return Transform.translate(
              offset: value,
              child: child,
            );
          },
          child: GestureDetector(
            // 가로 방향으로 드래그할 때 콜백 함수
            onHorizontalDragUpdate: (details) {
              // 드래그한 타일의 인덱스 설정
              dragI = i;
              dragJ = j;

              // 드래그한 방향에 따라 인접한 타일의 인덱스 설정
              if (details.delta.dx > 0) {
                // 오른쪽으로 드래그하면 오른쪽 타일과 교환
                swapI = i;
                swapJ = j + 1;
              } else if (details.delta.dx < 0) {
                // 왼쪽으로 드래그하면 왼쪽 타일과 교환
                swapI = i;
                swapJ = j - 1;
              }

              // 인접한 타일이 유효한 범위에 있으면 교환
              if (swapI != null &&
                  swapJ != null &&
                  swapI! >= 0 &&
                  swapI! < 7 &&
                  swapJ! >= 0 &&
                  swapJ! < 7) {
                swapTiles(dragI!, dragJ!, swapI!, swapJ!);
              }
            },
            onVerticalDragUpdate: (details) {
              // 드래그한 타일의 인덱스 설정
              dragI = i;
              dragJ = j;

              // 드래그한 방향에 따라 인접한 타일의 인덱스 설정
              if (details.delta.dy > 0) {
                swapI = i + 1;
                swapJ = j;
              } else if (details.delta.dy < 0) {
                swapI = i - 1;
                swapJ = j;
              }

              // 인접한 타일이 유효한 범위에 있으면 교환
              if (swapI != null &&
                  swapJ != null &&
                  swapI! >= 0 &&
                  swapI! < 7 &&
                  swapJ! >= 0 &&
                  swapJ! < 7) {
                swapTiles(dragI!, dragJ!, swapI!, swapJ!);
              }
            },
            child: SizedBox(
              width: mTileWidth,
              height: mTileHeight,
              child: tiles[i][j].getImage(),
            ),
          ),
        );
      },
    );
  }
}
```

The source code quoted verbose but enough information how it works. It works well in first dragging, exchanges two tiles well. However, when more times dragging, previous exchange reverts back to original spots so four tiles are moving. I think Tween animation run again and again, the original spots reverts back to, so it runs that way. Any ideas how to repair this operation?