Skip to content

Commit 70b9568

Browse files
committed
add staggered grid support
1 parent fcda088 commit 70b9568

File tree

7 files changed

+200
-38
lines changed

7 files changed

+200
-38
lines changed

example/complex_list.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class ComplexListExample extends StatelessWidget {
2323

2424
return Scaffold(
2525
appBar: const CustomAppBar(
26-
title: 'ComplexList',
26+
title: 'Complex List',
2727
codePath: 'example/complex_list.dart',
2828
),
2929
body: ListView.builder(

example/dimension_ratio.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class DimensionRatioExample extends StatelessWidget {
1010
Widget build(BuildContext context) {
1111
return Scaffold(
1212
appBar: const CustomAppBar(
13-
title: 'DimensionRatio',
13+
title: 'Dimension Ratio',
1414
codePath: 'example/dimension_ratio.dart',
1515
),
1616
body: ConstraintLayout(

example/home.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'guideline.dart';
1010
import 'horizontal_list.dart';
1111
import 'percentage_layout.dart';
1212
import 'relative_id.dart';
13+
import 'staggered_grid.dart';
1314
import 'summary.dart';
1415
import 'vertical_list.dart';
1516
import 'wrapper_constraints.dart';
@@ -21,15 +22,16 @@ class ExampleHome extends StatelessWidget {
2122
'Summary': const SummaryExample(),
2223
'Guideline': const GuidelineExample(),
2324
'Barrier': const BarrierExample(),
24-
'ComplexList': const ComplexListExample(),
25+
'Complex List': const ComplexListExample(),
2526
'Badge': const BadgeExample(),
26-
'PercentageLayout': const PercentageLayoutExample(),
27-
'DimensionRatio': const DimensionRatioExample(),
27+
'Percentage Layout': const PercentageLayoutExample(),
28+
'Dimension Ratio': const DimensionRatioExample(),
2829
'Relative Id': const RelativeIdExample(),
2930
'Wrapper Constraints': const WrapperConstraintsExample(),
3031
'Grid': const GridExample(),
3132
'Horizontal List': const HorizontalListExample(),
3233
'Vertical List': const VerticalListExample(),
34+
'Staggered Grid': const StaggeredGridExample(),
3335
'Chain (Coming soon)': null,
3436
};
3537

@@ -50,7 +52,7 @@ class ExampleHome extends StatelessWidget {
5052
topCenterTo: parent,
5153
),
5254
...constraintGrid(
53-
id: ConstraintId('example_list'),
55+
id: ConstraintId('exampleList'),
5456
margin: const EdgeInsets.only(
5557
top: 20,
5658
),

example/percentage_layout.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class PercentageLayoutExample extends StatelessWidget {
1010
Widget build(BuildContext context) {
1111
return Scaffold(
1212
appBar: const CustomAppBar(
13-
title: 'PercentageLayout',
13+
title: 'Percentage Layout',
1414
codePath: 'example/percentage_layout.dart',
1515
),
1616
body: ConstraintLayout(

example/staggered_grid.dart

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import 'dart:math';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_constraintlayout/src/constraint_layout.dart';
5+
6+
import 'custom_app_bar.dart';
7+
8+
class StaggeredGridExample extends StatelessWidget {
9+
const StaggeredGridExample({Key? key}) : super(key: key);
10+
11+
@override
12+
Widget build(BuildContext context) {
13+
List<Color> colors = [
14+
Colors.redAccent,
15+
Colors.greenAccent,
16+
Colors.blueAccent,
17+
Colors.orangeAccent,
18+
Colors.yellow,
19+
Colors.pink,
20+
Colors.lightBlueAccent
21+
];
22+
const double smallestSize = 40;
23+
const int columnCount = 8;
24+
Random random = Random();
25+
return Scaffold(
26+
appBar: const CustomAppBar(
27+
title: 'Staggered Grid',
28+
codePath: 'example/staggered_grid.dart',
29+
),
30+
body: ConstraintLayout(
31+
children: [
32+
TextButton(
33+
onPressed: () {
34+
(context as Element).markNeedsBuild();
35+
},
36+
child: const Text(
37+
'Upset',
38+
style: TextStyle(
39+
fontSize: 32,
40+
height: 1.5,
41+
),
42+
),
43+
).applyConstraint(
44+
left: ConstraintId('horizontalList').right,
45+
top: ConstraintId('horizontalList').top,
46+
),
47+
...constraintGrid(
48+
id: ConstraintId('horizontalList'),
49+
left: parent.left,
50+
top: parent.top,
51+
margin: const EdgeInsets.only(
52+
left: 100,
53+
),
54+
itemCount: 50,
55+
columnCount: columnCount,
56+
itemBuilder: (index) {
57+
return Container(
58+
color: colors[index % colors.length],
59+
alignment: Alignment.center,
60+
child: Text('$index'),
61+
);
62+
},
63+
itemSizeBuilder: (index) {
64+
if (index == 0) {
65+
return const Size(
66+
smallestSize * columnCount + 35, smallestSize);
67+
}
68+
if (index == 6) {
69+
return const Size(smallestSize * 2 + 5, smallestSize);
70+
}
71+
if (index == 7) {
72+
return const Size(smallestSize * 6 + 25, smallestSize);
73+
}
74+
if (index == 19) {
75+
return const Size(smallestSize * 2 + 5, smallestSize);
76+
}
77+
if (index == 29) {
78+
return const Size(smallestSize * 3 + 10, smallestSize);
79+
}
80+
return Size(
81+
smallestSize, (2 + random.nextInt(4)) * smallestSize);
82+
},
83+
itemSpanBuilder: (index) {
84+
if (index == 0) {
85+
return columnCount;
86+
}
87+
if (index == 6) {
88+
return 2;
89+
}
90+
if (index == 7) {
91+
return 6;
92+
}
93+
if (index == 19) {
94+
return 2;
95+
}
96+
if (index == 29) {
97+
return 3;
98+
}
99+
return 1;
100+
},
101+
itemMarginBuilder: (index) {
102+
return const EdgeInsets.only(
103+
left: 5,
104+
top: 5,
105+
);
106+
})
107+
],
108+
),
109+
);
110+
}
111+
}

lib/src/constraint_layout.dart

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'dart:collection';
22
import 'dart:convert';
3-
import 'dart:math';
43
import 'dart:ui' as ui;
54
import 'dart:ui';
65

@@ -104,6 +103,7 @@ List<Widget> constraintGrid({
104103
Size Function(int index)? itemSizeBuilder,
105104
required Widget Function(int index) itemBuilder,
106105
EdgeInsets Function(int index)? itemMarginBuilder,
106+
int Function(int index)? itemSpanBuilder,
107107
EdgeInsets margin = EdgeInsets.zero,
108108
CLVisibility visibility = visible,
109109
Offset translate = Offset.zero,
@@ -127,47 +127,91 @@ List<Widget> constraintGrid({
127127
EdgeInsets topMargin = EdgeInsets.only(
128128
top: margin.top,
129129
);
130-
EdgeInsets calculateItemMargin(
131-
int index,
132-
int columnCount,
133-
EdgeInsets margin,
134-
) {
135-
/// First row
136-
if (index < columnCount) {
137-
margin = margin.add(topMargin) as EdgeInsets;
138-
}
139-
140-
/// First column
141-
if (index % columnCount == 0) {
142-
margin = margin.add(leftMargin) as EdgeInsets;
143-
}
144-
return margin;
145-
}
146130

131+
List<ConstraintId> allChildIds = [];
147132
List<ConstraintId> leftChildIds = [];
148133
List<ConstraintId> topChildIds = [];
149134
List<ConstraintId> rightChildIds = [];
150135
List<ConstraintId> bottomChildIds = [];
151-
int lastRowIndex = (itemCount / columnCount).ceil() - 1;
152-
int lastColumnIndex = min(columnCount - 1, itemCount - 1);
136+
int totalAvailableSpanCount = (itemCount / columnCount).ceil() * columnCount;
137+
int currentRowIndex = -1;
138+
int currentRowUsedSpanCount = columnCount + 1;
139+
int totalUsedSpanCount = 0;
140+
late int currentRowBarrierCount;
141+
Map<int, ConstraintId> currentSpanSlot = HashMap();
153142
for (int i = 0; i < itemCount; i++) {
154143
ConstraintId itemId = ConstraintId(id.id + '_grid_item_$i');
155-
if (i % columnCount == 0) {
144+
allChildIds.add(itemId);
145+
146+
EdgeInsets childMargin = itemMarginBuilder?.call(i) ?? EdgeInsets.zero;
147+
int itemSpan = itemSpanBuilder?.call(i) ?? 1;
148+
assert(itemSpan >= 1 && itemSpan <= columnCount);
149+
currentRowUsedSpanCount += itemSpan;
150+
totalUsedSpanCount += itemSpan;
151+
152+
/// New row start
153+
if (currentRowUsedSpanCount > columnCount) {
154+
currentRowIndex++;
155+
currentRowUsedSpanCount = itemSpan;
156+
currentRowBarrierCount = 0;
157+
if (i > 0) {
158+
if (!rightChildIds.contains(allChildIds[i - 1])) {
159+
/// Last column
160+
rightChildIds.add(allChildIds[i - 1]);
161+
}
162+
} else {
163+
if (itemSpan == columnCount) {
164+
/// Last column
165+
rightChildIds.add(itemId);
166+
}
167+
}
168+
169+
/// First column
170+
leftAnchor = left;
156171
leftChildIds.add(itemId);
172+
childMargin = childMargin.add(leftMargin) as EdgeInsets;
157173
}
158-
if (i < columnCount) {
174+
175+
// First row
176+
if (currentRowIndex == 0) {
177+
childMargin = childMargin.add(topMargin) as EdgeInsets;
159178
topChildIds.add(itemId);
160179
}
161-
if (i % columnCount == lastColumnIndex) {
162-
rightChildIds.add(itemId);
163-
}
164-
if (i ~/ columnCount == lastRowIndex) {
180+
181+
// Last row
182+
if (totalAvailableSpanCount - totalUsedSpanCount < columnCount) {
165183
bottomChildIds.add(itemId);
166184
}
185+
186+
if (currentRowIndex > 0) {
187+
if (itemSpan == 1) {
188+
topAnchor = currentSpanSlot[currentRowUsedSpanCount]!.bottom;
189+
} else {
190+
List<ConstraintId> referencedIds = [];
191+
for (int i = 0; i < itemSpan; i++) {
192+
ConstraintId id = currentSpanSlot[currentRowUsedSpanCount - i]!;
193+
if (!referencedIds.contains(id)) {
194+
referencedIds.add(id);
195+
}
196+
}
197+
ConstraintId rowBarrierId = ConstraintId(id.id +
198+
'_row_${currentRowIndex}_bottom_barrier_$currentRowBarrierCount');
199+
Barrier rowBottomBarrier = Barrier(
200+
id: rowBarrierId,
201+
direction: BarrierDirection.bottom,
202+
referencedIds: referencedIds,
203+
);
204+
widgets.add(rowBottomBarrier);
205+
topAnchor = rowBarrierId.bottom;
206+
currentRowBarrierCount++;
207+
}
208+
}
209+
167210
Widget widget = itemBuilder(i);
168211
Size? itemSize = itemSizeBuilder?.call(i);
169212
double width = itemWidth ?? itemSize!.width;
170213
double height = itemHeight ?? itemSize!.height;
214+
171215
widgets.add(Constrained(
172216
child: widget,
173217
constraint: Constraint(
@@ -179,17 +223,21 @@ List<Widget> constraintGrid({
179223
zIndex: zIndex,
180224
translate: translate,
181225
visibility: visibility,
182-
margin: calculateItemMargin(
183-
i, columnCount, itemMarginBuilder?.call(i) ?? EdgeInsets.zero),
226+
margin: childMargin,
227+
goneMargin: childMargin,
184228
),
185229
));
230+
186231
leftAnchor = itemId.right;
187-
if (i % columnCount == columnCount - 1) {
188-
leftAnchor = left;
189-
topAnchor = itemId.bottom;
232+
for (int i = 0; i < itemSpan; i++) {
233+
currentSpanSlot[currentRowUsedSpanCount - i] = itemId;
190234
}
191235
}
192236

237+
if (!rightChildIds.contains(allChildIds.last)) {
238+
rightChildIds.add(allChildIds.last);
239+
}
240+
193241
Barrier leftBarrier = Barrier(
194242
id: ConstraintId(id.id + '_left_barrier'),
195243
direction: BarrierDirection.left,

pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ flutter:
3333
- example/wrapper_constraints.dart
3434
- example/grid.dart
3535
- example/horizontal_list.dart
36-
- example/vertical_list.dart
36+
- example/vertical_list.dart
37+
- example/staggered_grid.dart

0 commit comments

Comments
 (0)