Skip to content

Instantly share code, notes, and snippets.

@martynov-alex
Last active October 30, 2023 04:25
Show Gist options
  • Select an option

  • Save martynov-alex/49986fe6f7f7284969272043a10fa21e to your computer and use it in GitHub Desktop.

Select an option

Save martynov-alex/49986fe6f7f7284969272043a10fa21e to your computer and use it in GitHub Desktop.
Surf UI Quiz #1
import 'dart:math' as math;
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
home: const MultiChildExample(),
debugShowCheckedModeBanner: false,
);
}
}
enum _Widget {
backgroundImage,
cardsList,
}
class MultiChildExample extends StatefulWidget {
const MultiChildExample({super.key});
@override
State<MultiChildExample> createState() => _MultiChildExampleState();
}
class _MultiChildExampleState extends State<MultiChildExample> {
final _listKey = GlobalKey<AnimatedListState>();
final _duration = const Duration(milliseconds: 200);
final _cards = <_Card>[];
@override
void initState() {
super.initState();
_cards.addAll([for (var i = 0; i < 3; i++) _getCard(i)]);
}
_Card _getCard(int index) {
final iconCodePoint = 58000 + math.Random().nextInt(1000);
return _Card(index: index, iconCodePoint: iconCodePoint);
}
void _addItem() {
final nextIndex = _cards.length;
final newCard = _getCard(nextIndex);
_cards.insert(nextIndex, newCard);
_listKey.currentState!.insertItem(
nextIndex,
duration: _duration,
);
}
void _removeItem() {
if (_cards.length <= 2) return;
final lastIndex = _cards.length - 1;
final lastCard = _cards[lastIndex];
_listKey.currentState!.removeItem(
lastIndex,
duration: _duration,
(_, animation) => SizeTransition(
sizeFactor: animation,
child: lastCard,
),
);
_cards.removeAt(lastIndex);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomMultiChildLayout(
delegate: _MultiChildExampleDelegate(
minOverlap: 0.1,
maxOverlap: 0.7,
),
children: [
LayoutId(
id: _Widget.backgroundImage,
child: const ColoredBox(color: Colors.redAccent),
),
LayoutId(
id: _Widget.cardsList,
child: Padding(
padding: const EdgeInsets.all(16),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.greenAccent.withOpacity(0.9),
borderRadius: BorderRadius.circular(16),
),
child: AnimatedList(
key: _listKey,
padding: const EdgeInsets.all(16),
primary: false,
shrinkWrap: true,
initialItemCount: _cards.length,
itemBuilder: (_, index, animation) => SizeTransition(
sizeFactor: animation,
child: _cards[index],
),
),
),
),
),
],
),
floatingActionButton: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
backgroundColor: Colors.yellow,
onPressed: () => _addItem(),
child: const Icon(Icons.add),
),
const SizedBox(height: 16),
FloatingActionButton(
backgroundColor: Colors.yellow,
onPressed: () => _removeItem(),
child: const Icon(Icons.remove),
),
],
),
),
);
}
}
class _Card extends StatelessWidget {
final int index;
final int iconCodePoint;
const _Card({required this.index, required this.iconCodePoint});
@override
Widget build(BuildContext context) {
final card = index == 0
? const Center(
child: Text(
'Surf UI Quiz #1',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Card $index'),
index == 1
? const Icon(Icons.flutter_dash)
: Icon(IconData(iconCodePoint, fontFamily: 'MaterialIcons')),
],
);
return SizedBox(
height: 40,
child: Center(child: card),
);
}
}
class _MultiChildExampleDelegate extends MultiChildLayoutDelegate {
/// Коэффициент минимального перекрытия виджета фона виджетом списка.
final double minOverlap;
/// Коэффициент максимального перекрытия виджета фона виджетом списка.
final double maxOverlap;
_MultiChildExampleDelegate({
required this.minOverlap,
required this.maxOverlap,
}) : assert(minOverlap >= 0.0 && minOverlap <= 1.0),
assert(maxOverlap >= 0.0 && maxOverlap <= 1.0),
assert(minOverlap < maxOverlap);
@override
void performLayout(Size size) {
// Задаем ограничения для виджета фона.
// Высота равна половине доступной от родителя высоты, в нашем случае половине высоты всего экрана.
final backgroundImageMaxHeight = size.height / 2;
final backgroundImageSize = Size(size.width, backgroundImageMaxHeight);
layoutChild(
_Widget.backgroundImage,
BoxConstraints.tight(backgroundImageSize),
);
// Задаем ограничения для виджета списка.
// Максимальная высота определяется коэффициентом максимального перекрытия виджета фона.
final cardsListSize = Size(
size.width,
backgroundImageMaxHeight * (1 + maxOverlap),
);
final cardsListLayout = layoutChild(
_Widget.cardsList,
BoxConstraints.loose(cardsListSize),
);
// Определяем смещение виджета списка.
// Тут мы смотрим на минимальное значение между двух величин:
// - offsetDependsOnMinOverlap — это смещение, которое зависит от коэффициента минимального
// перекрытия и оно будет меньше, когда список маленький.
// - offsetDependsOnCardListHeight — это смещение, которое зависит от высоты списка и оно
// будет меньше, когда список будет расти.
//
// Т.е. поведение списка (растет вверх или вниз) определяется пересчетом и сравнением указанных
// величин.
final offsetDependsOnMinOverlap = backgroundImageMaxHeight * (1 - minOverlap);
final offsetDependsOnCardListHeight = size.height - cardsListLayout.height;
positionChild(
_Widget.cardsList,
Offset(
0,
math.min(offsetDependsOnMinOverlap, offsetDependsOnCardListHeight),
),
);
}
@override
bool shouldRelayout(covariant _MultiChildExampleDelegate oldDelegate) {
return oldDelegate.minOverlap != minOverlap || oldDelegate.maxOverlap != maxOverlap;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment