mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
230 lines
7.0 KiB
Dart
230 lines
7.0 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Flutter code sample for [SliverAnimatedGrid].
|
|
|
|
void main() => runApp(const SliverAnimatedGridSample());
|
|
|
|
class SliverAnimatedGridSample extends StatefulWidget {
|
|
const SliverAnimatedGridSample({super.key});
|
|
|
|
@override
|
|
State<SliverAnimatedGridSample> createState() => _SliverAnimatedGridSampleState();
|
|
}
|
|
|
|
class _SliverAnimatedGridSampleState extends State<SliverAnimatedGridSample> {
|
|
final GlobalKey<SliverAnimatedGridState> _listKey = GlobalKey<SliverAnimatedGridState>();
|
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
|
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
|
late ListModel<int> _list;
|
|
int? _selectedItem;
|
|
late int _nextItem; // The next item inserted when the user presses the '+' button.
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_list = ListModel<int>(
|
|
listKey: _listKey,
|
|
initialItems: <int>[0, 1, 2, 3, 4, 5],
|
|
removedItemBuilder: _buildRemovedItem,
|
|
);
|
|
_nextItem = 6;
|
|
}
|
|
|
|
// Used to build list items that haven't been removed.
|
|
Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
|
|
return CardItem(
|
|
animation: animation,
|
|
item: _list[index],
|
|
selected: _selectedItem == _list[index],
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Used to build an item after it has been removed from the list. This
|
|
// method is needed because a removed item remains visible until its
|
|
// animation has completed (even though it's gone as far this ListModel is
|
|
// concerned). The widget will be used by the
|
|
// [AnimatedGridState.removeItem] method's
|
|
// [AnimatedGridRemovedItemBuilder] parameter.
|
|
Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
|
|
return CardItem(
|
|
animation: animation,
|
|
removing: true,
|
|
item: item,
|
|
);
|
|
}
|
|
|
|
// Insert the "next item" into the list model.
|
|
void _insert() {
|
|
final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
|
|
_list.insert(index, _nextItem++);
|
|
}
|
|
|
|
// Remove the selected item from the list model.
|
|
void _remove() {
|
|
if (_selectedItem != null) {
|
|
_list.removeAt(_list.indexOf(_selectedItem!));
|
|
} else {
|
|
_list.removeAt(_list.length - 1);
|
|
}
|
|
setState(() {
|
|
_selectedItem = null;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
scaffoldMessengerKey: _scaffoldMessengerKey,
|
|
debugShowCheckedModeBanner: false,
|
|
home: Scaffold(
|
|
key: _scaffoldKey,
|
|
body: CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverAppBar(
|
|
title: const Text(
|
|
'SliverAnimatedGrid',
|
|
style: TextStyle(fontSize: 30),
|
|
),
|
|
expandedHeight: 60,
|
|
centerTitle: true,
|
|
backgroundColor: Colors.amber[900],
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.remove_circle),
|
|
onPressed: _remove,
|
|
tooltip: 'Remove the selected item, or the last item if none selected.',
|
|
iconSize: 32,
|
|
),
|
|
actions: <Widget>[
|
|
IconButton(
|
|
icon: const Icon(Icons.add_circle),
|
|
onPressed: _insert,
|
|
tooltip: 'Insert a new item.',
|
|
iconSize: 32,
|
|
),
|
|
],
|
|
),
|
|
SliverAnimatedGrid(
|
|
key: _listKey,
|
|
initialItemCount: _list.length,
|
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
maxCrossAxisExtent: 100.0,
|
|
mainAxisSpacing: 10.0,
|
|
crossAxisSpacing: 10.0,
|
|
),
|
|
itemBuilder: _buildItem,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
typedef RemovedItemBuilder = Widget Function(int item, BuildContext context, Animation<double> animation);
|
|
|
|
// Keeps a Dart [List] in sync with an [AnimatedGrid].
|
|
//
|
|
// The [insert] and [removeAt] methods apply to both the internal list and
|
|
// the animated list that belongs to [listKey].
|
|
//
|
|
// This class only exposes as much of the Dart List API as is needed by the
|
|
// sample app. More list methods are easily added, however methods that
|
|
// mutate the list must make the same changes to the animated list in terms
|
|
// of [AnimatedGridState.insertItem] and [AnimatedGrid.removeItem].
|
|
class ListModel<E> {
|
|
ListModel({
|
|
required this.listKey,
|
|
required this.removedItemBuilder,
|
|
Iterable<E>? initialItems,
|
|
}) : _items = List<E>.from(initialItems ?? <E>[]);
|
|
|
|
final GlobalKey<SliverAnimatedGridState> listKey;
|
|
final RemovedItemBuilder removedItemBuilder;
|
|
final List<E> _items;
|
|
|
|
SliverAnimatedGridState get _animatedGrid => listKey.currentState!;
|
|
|
|
void insert(int index, E item) {
|
|
_items.insert(index, item);
|
|
_animatedGrid.insertItem(index);
|
|
}
|
|
|
|
E removeAt(int index) {
|
|
final E removedItem = _items.removeAt(index);
|
|
if (removedItem != null) {
|
|
_animatedGrid.removeItem(
|
|
index,
|
|
(BuildContext context, Animation<double> animation) => removedItemBuilder(index, context, animation),
|
|
);
|
|
}
|
|
return removedItem;
|
|
}
|
|
|
|
int get length => _items.length;
|
|
|
|
E operator [](int index) => _items[index];
|
|
|
|
int indexOf(E item) => _items.indexOf(item);
|
|
}
|
|
|
|
// Displays its integer item as 'Item N' on a Card whose color is based on
|
|
// the item's value.
|
|
//
|
|
// The card turns gray when [selected] is true. This widget's height
|
|
// is based on the [animation] parameter. It varies as the animation value
|
|
// transitions from 0.0 to 1.0.
|
|
class CardItem extends StatelessWidget {
|
|
const CardItem({
|
|
super.key,
|
|
this.onTap,
|
|
this.selected = false,
|
|
this.removing = false,
|
|
required this.animation,
|
|
required this.item,
|
|
}) : assert(item >= 0);
|
|
|
|
final Animation<double> animation;
|
|
final VoidCallback? onTap;
|
|
final int item;
|
|
final bool selected;
|
|
final bool removing;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 2.0,
|
|
right: 2.0,
|
|
top: 2.0,
|
|
),
|
|
child: ScaleTransition(
|
|
scale: CurvedAnimation(parent: animation, curve: removing ? Curves.easeInOut : Curves.bounceOut),
|
|
child: GestureDetector(
|
|
onTap: onTap,
|
|
child: SizedBox(
|
|
height: 80.0,
|
|
child: Card(
|
|
color: selected ? Colors.black12 : Colors.primaries[item % Colors.primaries.length],
|
|
child: Center(
|
|
child: Text(
|
|
(item + 1).toString(),
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|