two_way_scrollable 1.0.0 copy "two_way_scrollable: ^1.0.0" to clipboard
two_way_scrollable: ^1.0.0 copied to clipboard

A set of two-way growable widgets for Flutter that properly fill the viewport even if there is not enough content.

example/lib/main.dart

import 'dart:async';

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:random_color/random_color.dart';
import 'package:two_way_scrollable/two_way_scrollable.dart';
import 'package:window_size/window_size.dart';

const _macosTitlebarHeight = 22.0;
const _windowSize = Size(300, 600 + _macosTitlebarHeight);

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
  setWindowMinSize(_windowSize);
  setWindowMaxSize(_windowSize);
  runApp(const SandboxApp());
}

class SandboxApp extends StatelessWidget {
  const SandboxApp({
    super.key,
    this.anchor = TwoWayListViewAnchor.top,
    this.direction = TwoWayListViewDirection.topToBottom,
  });

  final TwoWayListViewAnchor anchor;
  final TwoWayListViewDirection direction;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: _Content(
        anchor: anchor,
        direction: direction,
      ),
    );
  }
}

class _Content extends StatefulWidget {
  const _Content({
    Key? key,
    required this.anchor,
    required this.direction,
  }) : super(key: key);

  final TwoWayListViewAnchor anchor;
  final TwoWayListViewDirection direction;

  @override
  State<_Content> createState() => _ContentState();
}

class _ContentState extends State<_Content> {
  var ctrl = TwoWayListViewController<int>();

  late var anchor = widget.anchor;
  late var direction = widget.direction;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: buildAppBar(),
      body: RepaintBoundary(
        child: Stack(
          key: const Key('TwoWayListView'),
          children: [
            Container(color: Colors.white),
            buildListView(),
          ],
        ),
      ),
    );
  }

  AppBar buildAppBar() {
    return AppBar(
      centerTitle: false,
      actions: [
        const Icon(MdiIcons.plusBox),
        InkResponse(
          key: const ValueKey('add-first'),
          onTap: () {
            final first = ctrl.items.firstOrNull;
            ctrl.insert(-1, first != null ? first - 1 : -1);
          },
          onLongPress: () {
            final first = ctrl.items.firstOrNull ?? 0;
            final items = List.generate(10, (i) => first - i - 1);
            ctrl.insertAll(-1, items.reversed.toList());
          },
          child: const Icon(MdiIcons.triangleSmallUp),
        ),
        InkResponse(
          key: const ValueKey('add-last'),
          onTap: () {
            final last = ctrl.items.lastOrNull;
            ctrl.insert(ctrl.items.length, last != null ? last + 1 : 0);
          },
          onLongPress: () {
            final last = ctrl.items.lastOrNull ?? -1;
            final items = List.generate(10, (i) => last + i + 1);
            ctrl.insertAll(ctrl.items.length, items);
          },
          child: const Icon(MdiIcons.triangleSmallDown),
        ),
        const SizedBox(width: 8),
        const Icon(MdiIcons.minusBox),
        InkResponse(
          key: const ValueKey('remove-first'),
          onTap: () {
            final item = ctrl.items.firstOrNull;
            if (item == null) return;
            ctrl.remove(item);
          },
          onLongPress: () {
            for (var i = 0; i < 10; i++) {
              final item = ctrl.items.firstOrNull;
              if (item == null) return;
              ctrl.remove(item);
            }
          },
          child: const Icon(MdiIcons.triangleSmallUp),
        ),
        InkResponse(
          key: const ValueKey('remove-last'),
          onTap: () {
            final item = ctrl.items.lastOrNull;
            if (item == null) return;
            ctrl.remove(item);
          },
          onLongPress: () {
            for (var i = 0; i < 10; i++) {
              final item = ctrl.items.lastOrNull;
              if (item == null) return;
              ctrl.remove(item);
            }
          },
          child: const Icon(MdiIcons.triangleSmallDown),
        ),
        const SizedBox(width: 8),
        InkResponse(
          key: const ValueKey('anchor'),
          onTap: () => setState(() {
            _swapAnchor();
          }),
          child: anchor == TwoWayListViewAnchor.top
              ? const Icon(MdiIcons.alignVerticalTop)
              : const Icon(MdiIcons.alignVerticalBottom),
        ),
        const SizedBox(width: 8),
        InkResponse(
          key: const ValueKey('direction'),
          onTap: () => setState(() {
            _swapDirection();
          }),
          child: direction == TwoWayListViewDirection.topToBottom
              ? const Icon(MdiIcons.sortAscending)
              : const Icon(MdiIcons.sortDescending),
        ),
        const SizedBox(width: 8),
        InkResponse(
          key: const ValueKey('reverse'),
          onTap: () => setState(() {
            _swapAnchor();
            _swapDirection();
          }),
          child: const Icon(MdiIcons.swapVertical),
        ),
        const SizedBox(width: 8),
        InkResponse(
          key: const ValueKey('refresh'),
          onTap: () => setState(() {
            ctrl = TwoWayListViewController<int>();
            direction = TwoWayListViewDirection.topToBottom;
            anchor = TwoWayListViewAnchor.top;
          }),
          child: const Icon(MdiIcons.refresh),
        ),
        const SizedBox(width: 16),
      ],
    );
  }

  void _swapAnchor() {
    switch (anchor) {
      case TwoWayListViewAnchor.top:
        anchor = TwoWayListViewAnchor.bottom;
        break;
      case TwoWayListViewAnchor.bottom:
        anchor = TwoWayListViewAnchor.top;
        break;
    }
  }

  void _swapDirection() {
    switch (direction) {
      case TwoWayListViewDirection.topToBottom:
        direction = TwoWayListViewDirection.bottomToTop;
        break;
      case TwoWayListViewDirection.bottomToTop:
        direction = TwoWayListViewDirection.topToBottom;
        break;
    }
  }

  Widget buildListView() {
    return TwoWayListView(
      controller: ctrl,
      anchor: anchor,
      direction: direction,
      topSlivers: const [
        SliverToBoxAdapter(
          child: _DebugListBoundaryIndicator(),
        ),
        SliverToBoxAdapter(
          child: SizedBox(height: 16),
        ),
      ],
      centerSliver: const SliverToBoxAdapter(
        child: _DebugCenterIndicator(),
      ),
      bottomSlivers: const [
        SliverToBoxAdapter(
          child: SizedBox(height: 16),
        ),
        SliverToBoxAdapter(
          child: _DebugListBoundaryIndicator(),
        ),
      ],
      itemBuilder: (context, index, item, anim) =>
          _Item(ctrl: ctrl, item: item, animation: anim),
    );
  }
}

class _DebugCenterIndicator extends StatelessWidget {
  const _DebugCenterIndicator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 4,
      child: Row(
        mainAxisSize: MainAxisSize.max,
        children: [
          Expanded(child: Container(color: Colors.red)),
          Expanded(child: Container(color: Colors.orange)),
          Expanded(child: Container(color: Colors.yellow)),
          Expanded(child: Container(color: Colors.green)),
          Expanded(child: Container(color: Colors.lightBlue)),
          Expanded(child: Container(color: Colors.blue)),
          Expanded(child: Container(color: Colors.purple)),
        ],
      ),
    );
  }
}

class _DebugListBoundaryIndicator extends StatelessWidget {
  const _DebugListBoundaryIndicator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(height: 4, color: Colors.red);
  }
}

class _Item extends StatefulWidget {
  const _Item({
    Key? key,
    required this.ctrl,
    required this.item,
    required this.animation,
  }) : super(key: key);

  final TwoWayListViewController<int> ctrl;
  final int item;
  final Animation<double> animation;

  @override
  State<_Item> createState() => _ItemState();
}

class _ItemState extends State<_Item> {
  var initialized = false;

  @override
  void initState() {
    super.initState();
    Timer(const Duration(milliseconds: 300), () {
      if (mounted) {
        setState(() {
          initialized = true;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final itemIndex = widget.ctrl.items.indexOf(widget.item);
    final centerIndex = widget.ctrl.centerIndex;

    final rand = RandomColor(widget.item);
    late final Color color;
    if (itemIndex < 0) {
      color = Colors.grey[700]!;
    } else if (itemIndex < centerIndex) {
      color = rand.randomColor(
        colorHue: ColorHue.yellow,
        colorBrightness: ColorBrightness.veryLight,
        colorSaturation: ColorSaturation.mediumSaturation,
      );
    } else {
      color = rand.randomColor(
        colorHue: ColorHue.blue,
        colorBrightness: ColorBrightness.veryLight,
        colorSaturation: ColorSaturation.mediumSaturation,
      );
    }

    return SizeTransition(
      sizeFactor: widget.animation,
      axisAlignment: 0,
      child: Container(
        alignment: Alignment.center,
        color: color,
        height: 100 + widget.item % 4 * 60.0,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('Item: ${widget.item}'),
            if (!initialized)
              Container(
                width: 16,
                height: 16,
                margin: const EdgeInsets.only(left: 16, right: 16),
                child: const CircularProgressIndicator(color: Colors.black),
              ),
            if (initialized)
              IconButton(
                key: ValueKey('remove:${widget.item}'),
                icon: const Icon(Icons.delete),
                onPressed: () => widget.ctrl.remove(widget.item),
              )
          ],
        ),
      ),
    );
  }
}
8
likes
160
points
44
downloads

Publisher

verified publishersonerik.dev

Weekly Downloads

A set of two-way growable widgets for Flutter that properly fill the viewport even if there is not enough content.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter, meta

More

Packages that depend on two_way_scrollable