animated_containers 1.1.0 copy "animated_containers: ^1.1.0" to clipboard
animated_containers: ^1.1.0 copied to clipboard

Flex and Wrap widgets with animated insertion, deletion, and layout changes.

example/lib/main.dart

import 'dart:math';
import 'dart:ui';

import 'package:animated_containers/animated_containers.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_touch_ripple/components/touch_ripple_context.dart';
import 'package:flutter_touch_ripple/widgets/touch_ripple.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AnimatedWrap Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
            seedColor: const Color.fromARGB(255, 34, 34, 34)),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

enum DemoOrientation {
  normal,
  weird,
}

class _MyHomePageState extends State<MyHomePage> {
  final _random = Random();
  final List<_Item> _items = [];
  int _nextId = 0;
  final FocusNode _focusNode = FocusNode();
  int _insertButtonPressCount = 0;
  DemoOrientation _orientation = DemoOrientation.normal;
  @override
  void initState() {
    super.initState();
    // Add initial items
    for (int i = 0; i < 14; i++) {
      _items.add(_createRandomItem());
    }
  }

  // note, irl, you should probably use [FIC IList](https://pub.dev/packages/fast_immutable_collections#fast-immutable-collections)s instead of modifying a List like this. Doing it this way, we have to clone the list every time we build, which makes rebuilding a bit less efficient. But for a code example I'll just use the simple datastructure that everyone already has.
  _Item _createRandomItem() {
    final (Color, Color) colors = _getRandomColors(_random);
    final mid = _nextId++;
    return _Item(
      id: mid,
      key: ValueKey(mid),
      width: lengthDistribution[_random.nextInt(lengthDistribution.length)],
      backgroundColor: colors.$1,
      color: colors.$2,
      onTap: () => _removeItem(mid),
    );
  }

  void _removeItem(int id) {
    setState(() {
      _items.removeWhere((item) => item.id == id);
    });
  }

  void _insertThreeItems() {
    setState(() {
      for (int i = 0; i < 3; i++) {
        final insertIndex = _random.nextInt(_items.length + 1);
        _items.insert(insertIndex, _createRandomItem());
      }
    });
  }

  void _removeFirstItem() {
    if (_items.isNotEmpty) {
      setState(() {
        _items.removeAt(0);
      });
    }
  }

  void _insertOneItem() {
    setState(() {
      int insertPosition = 3 * _insertButtonPressCount;
      if (insertPosition >= _items.length) {
        _insertButtonPressCount = 0;
        insertPosition = 0;
      }
      _items.insert(insertPosition, _createRandomItem());
      _insertButtonPressCount++;
    });
  }

  void _shiftOne() {
    setState(() {
      final removed = _items.removeAt(_random.nextInt(_items.length));
      // +1 because after the end is a valid position too
      _items.insert(_random.nextInt(_items.length + 1), removed);
    });
  }

  void _swapSome(int nToSwap) {
    // don't swap more items than there are
    nToSwap = min(nToSwap, _items.length);
    setState(() {
      final indices = [];
      for (int i = 0; i < nToSwap; i++) {
        int ni;
        // ensure all indices are unique
        do {
          ni = _random.nextInt(_items.length);
        } while (indices.contains(ni));
        indices.add(ni);
      }
      // swap the items
      final temp = _items[indices[0]];
      for (int i = 0; i < nToSwap - 1; i++) {
        _items[indices[i]] = _items[indices[i + 1]];
      }
      _items[indices[nToSwap - 1]] = temp;
    });
  }

  void _resizeOne() {
    setState(() {
      final int index = _random.nextInt(_items.length);
      final _Item prev = _items[index];
      double width;
      // repeat until we get a new width
      do {
        width = lengthDistribution[_random.nextInt(lengthDistribution.length)];
      } while (prev.width == width);
      _items[index] = _Item(
        id: prev.id,
        key: prev.key,
        width: width,
        backgroundColor: prev.backgroundColor,
        color: prev.color,
        onTap: prev.onTap,
      );
    });
  }

  void _toggleOrientation() {
    setState(() {
      _orientation = _orientation == DemoOrientation.normal
          ? DemoOrientation.weird
          : DemoOrientation.normal;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('animated wrap'),
      ),
      body: KeyboardListener(
        focusNode: _focusNode,
        onKeyEvent: (event) {
          if (event is KeyDownEvent) {
            if (event.logicalKey == LogicalKeyboardKey.backspace) {
              _removeFirstItem();
            } else if (event.logicalKey == LogicalKeyboardKey.digit1) {
              _insertOneItem();
            } else if (event.logicalKey == LogicalKeyboardKey.digit3) {
              _insertThreeItems();
            } else if (event.logicalKey == LogicalKeyboardKey.space) {
              _swapSome(3);
            }
          }
        },
        autofocus: true,
        child: Container(
          constraints: const BoxConstraints.expand(),
          child: Stack(
            alignment: Alignment.bottomCenter,
            children: [
              Container(
                constraints: const BoxConstraints.expand(),
                child: switch (_orientation) {
                  DemoOrientation.normal => SingleChildScrollView(
                      scrollDirection: Axis.vertical,
                      padding: const EdgeInsets.all(8.0),
                      child: AnimatedWrap.material3(
                        spacing: 8,
                        runSpacing: 8,
                        staggeredInitialInsertionAnimation:
                            const Duration(milliseconds: 29),
                        children: _items.toList(),
                      ),
                    ),
                  DemoOrientation.weird => SingleChildScrollView(
                      scrollDirection: Axis.horizontal,
                      padding: const EdgeInsets.all(8),
                      child: AnimatedWrap.material3(
                        direction: Axis.vertical,
                        alignment: WrapAlignment.start,
                        runAlignment: WrapAlignment.start,
                        crossAxisAlignment: AnimatedWrapCrossAlignment.start,
                        verticalDirection: VerticalDirection.up,
                        textDirection: TextDirection.rtl,
                        spacing: 2,
                        runSpacing: 14,
                        staggeredInitialInsertionAnimation:
                            const Duration(milliseconds: 29),
                        children: _items.toList(),
                      ),
                    )
                },
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: AnimatedWrap.material3(
                  alignment: WrapAlignment.center,
                  runAlignment: WrapAlignment.start,
                  verticalDirection: VerticalDirection.up,
                  spacing: 11,
                  runSpacing: 11,
                  children: [
                    ElevatedButton(
                      onPressed: _insertOneItem,
                      key: const ValueKey('insert one'),
                      child: const Text('insert one'),
                    ),
                    ElevatedButton(
                      onPressed: _insertThreeItems,
                      key: const ValueKey('insert three'),
                      child: const Text('insert three'),
                    ),
                    ElevatedButton(
                      onPressed: _shiftOne,
                      key: const ValueKey('shift one'),
                      child: const Text('shift one'),
                    ),
                    ElevatedButton(
                      onPressed: () => _swapSome(3),
                      key: const ValueKey('swap three'),
                      child: const Text('swap three'),
                    ),
                    ElevatedButton(
                      onPressed: () => _resizeOne(),
                      key: const ValueKey('resize one'),
                      child: const Text('resize one'),
                    ),
                    ElevatedButton(
                      onPressed: () => _toggleOrientation(),
                      key: const ValueKey('toggle orientation'),
                      child: Text(
                          'orientation: ${_orientation == DemoOrientation.normal ? 'normal' : 'weird'}'),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// todo: delete this I guess :( it doesn't look as nice as the default button
class OurButton extends StatelessWidget {
  final VoidCallback onPressed;
  final String text;
  static OurButton textKeyed(VoidCallback onPressed, String text) => OurButton(
        onPressed,
        text,
        key: ValueKey(text),
      );
  const OurButton(this.onPressed, this.text, {super.key});
  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    return Container(
      decoration: BoxDecoration(
        color: theme.colorScheme.surfaceDim,
        borderRadius: BorderRadius.circular(20),
      ),
      clipBehavior: Clip.antiAlias,
      child: ourTouchRipple(
        onTap: onPressed,
        color: theme.colorScheme.onSurface,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
          child: Text(text, style: theme.textTheme.bodyLarge),
        ),
      ),
    );
  }
}

class _Item extends StatelessWidget {
  final int id;
  final double width;
  final Color color;
  final Color backgroundColor;
  final VoidCallback onTap;

  const _Item({
    super.key,
    required this.id,
    required this.width,
    required this.color,
    required this.backgroundColor,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return RanimatedContainer(
      constraints: BoxConstraints(minWidth: width),
      animationDuration: material3MoveAnimationDuration,
      decoration: BoxDecoration(
        color: backgroundColor,
        borderRadius: BorderRadius.circular(8),
      ),
      child: ourTouchRipple(
        onTap: onTap,
        color: const Color.fromARGB(255, 255, 255, 255),
        child: Padding(
          padding: const EdgeInsets.symmetric(
            horizontal: 12.0,
            vertical: 8.0,
          ),
          child: Text(
            '$id',
            style: TextStyle(
              color: color,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}

const colors = [
  (Color(0xffcfeca2), Color(0xff3f5a11)),
  (Color.fromARGB(255, 240, 184, 233), Color(0xff670f5c)),
  (Color(0xffafe9ef), Color(0xff0b5359)),
  (Color(0xffefcaaf), Color(0xff5b3112)),
];

(Color, Color) _getRandomColors(Random random) {
  return colors[random.nextInt(colors.length)];
}

const List<double> lengthDistribution = [17.0, 35.0, 35.0, 60.0, 110.0];

Color lightenColor(Color color, double amount) {
  return Color.fromARGB(
    (color.a * 255).toInt(),
    (clampDouble(color.r + (1 - color.r) * amount, 0, 1) * 255).toInt(),
    (clampDouble(color.g + (1 - color.g) * amount, 0, 1) * 255).toInt(),
    (clampDouble(color.b + (1 - color.b) * amount, 0, 1) * 255).toInt(),
  );
}

Interval delayedCurve(
        {required Duration by,
        required Duration total,
        Curve curve = Curves.linear}) =>
    Interval(curve: curve, by.inMilliseconds / total.inMilliseconds, 1.0);

Widget ourTouchRipple({
  Key? key,
  TouchRippleShape? shape,
  required Widget child,
  Color color = const Color.fromARGB(255, 255, 255, 255),
  required VoidCallback onTap,
}) =>
    TouchRipple(
      key: key,
      cancelBehavior: TouchRippleCancelBehavior.none,
      onTap: onTap,
      hoverColor: color.withAlpha(40),
      rippleColor: color.withAlpha(100),
      child: child,
    );
0
likes
150
points
174
downloads

Publisher

unverified uploader

Weekly Downloads

Flex and Wrap widgets with animated insertion, deletion, and layout changes.

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

circular_reveal_animation, flutter, flutter_animate

More

Packages that depend on animated_containers