Skip to content

Commit 2848ecb

Browse files
authored
Merge pull request #472 from AppFlowy-IO/feat_row_expand
Feat: auto expand row height
2 parents 5e8c890 + 3435e42 commit 2848ecb

File tree

16 files changed

+410
-307
lines changed

16 files changed

+410
-307
lines changed

frontend/app_flowy/lib/workspace/application/grid/cell/cell_service.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,4 +365,11 @@ class GridCell with _$GridCell {
365365
required Field field,
366366
Cell? cell,
367367
}) = _GridCell;
368+
369+
// ignore: unused_element
370+
const GridCell._();
371+
372+
String cellId() {
373+
return rowId + field.id + "${field.fieldType}";
374+
}
368375
}

frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'dart:collection';
22
import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart';
3+
import 'package:equatable/equatable.dart';
4+
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
35
import 'package:flutter_bloc/flutter_bloc.dart';
46
import 'package:freezed_annotation/freezed_annotation.dart';
57
import 'dart:async';
@@ -28,7 +30,13 @@ class RowBloc extends Bloc<RowEvent, RowState> {
2830
_rowService.createRow();
2931
},
3032
didReceiveCellDatas: (_DidReceiveCellDatas value) async {
31-
emit(state.copyWith(cellDataMap: value.cellData));
33+
final fields = value.gridCellMap.values.map((e) => CellSnapshot(e.field)).toList();
34+
final snapshots = UnmodifiableListView(fields);
35+
emit(state.copyWith(
36+
gridCellMap: value.gridCellMap,
37+
snapshots: snapshots,
38+
changeReason: value.reason,
39+
));
3240
},
3341
);
3442
},
@@ -47,7 +55,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
4755
Future<void> _startListening() async {
4856
_rowListenFn = _rowCache.addRowListener(
4957
rowId: state.rowData.rowId,
50-
onUpdated: (cellDatas) => add(RowEvent.didReceiveCellDatas(cellDatas)),
58+
onUpdated: (cellDatas, reason) => add(RowEvent.didReceiveCellDatas(cellDatas, reason)),
5159
listenWhen: () => !isClosed,
5260
);
5361
}
@@ -57,18 +65,35 @@ class RowBloc extends Bloc<RowEvent, RowState> {
5765
class RowEvent with _$RowEvent {
5866
const factory RowEvent.initial() = _InitialRow;
5967
const factory RowEvent.createRow() = _CreateRow;
60-
const factory RowEvent.didReceiveCellDatas(GridCellMap cellData) = _DidReceiveCellDatas;
68+
const factory RowEvent.didReceiveCellDatas(GridCellMap gridCellMap, GridRowChangeReason reason) =
69+
_DidReceiveCellDatas;
6170
}
6271

6372
@freezed
6473
class RowState with _$RowState {
6574
const factory RowState({
6675
required GridRow rowData,
67-
required GridCellMap cellDataMap,
76+
required GridCellMap gridCellMap,
77+
required UnmodifiableListView<CellSnapshot> snapshots,
78+
GridRowChangeReason? changeReason,
6879
}) = _RowState;
6980

7081
factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState(
7182
rowData: rowData,
72-
cellDataMap: cellDataMap,
83+
gridCellMap: cellDataMap,
84+
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => CellSnapshot(e.field)).toList()),
7385
);
7486
}
87+
88+
class CellSnapshot extends Equatable {
89+
final Field _field;
90+
91+
const CellSnapshot(Field field) : _field = field;
92+
93+
@override
94+
List<Object?> get props => [
95+
_field.id,
96+
_field.fieldType,
97+
_field.visibility,
98+
];
99+
}

frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
4242
Future<void> _startListening() async {
4343
_rowListenFn = _rowCache.addRowListener(
4444
rowId: rowData.rowId,
45-
onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
45+
onUpdated: (cellDatas, reason) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
4646
listenWhen: () => !isClosed,
4747
);
4848
}

frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class GridRowCache {
8383

8484
RowUpdateCallback addRowListener({
8585
required String rowId,
86-
void Function(GridCellMap)? onUpdated,
86+
void Function(GridCellMap, GridRowChangeReason)? onUpdated,
8787
bool Function()? listenWhen,
8888
}) {
8989
listenrHandler() async {
@@ -99,7 +99,7 @@ class GridRowCache {
9999
final row = _rowsNotifier.rowDataWithId(rowId);
100100
if (row != null) {
101101
final GridCellMap cellDataMap = _makeGridCells(rowId, row);
102-
onUpdated(cellDataMap);
102+
onUpdated(cellDataMap, _rowsNotifier._changeReason);
103103
}
104104
}
105105

@@ -339,7 +339,7 @@ class GridRow with _$GridRow {
339339
const factory GridRow({
340340
required String gridId,
341341
required String rowId,
342-
required List<Field> fields,
342+
required UnmodifiableListView<Field> fields,
343343
required double height,
344344
Row? data,
345345
}) = _GridRow;

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class GridSize {
99
static double get leadingHeaderPadding => 50 * scale;
1010
static double get trailHeaderPadding => 140 * scale;
1111
static double get headerContainerPadding => 0 * scale;
12-
static double get cellHPadding => 10 * scale;
12+
static double get cellHPadding => 8 * scale;
1313
static double get cellVPadding => 8 * scale;
1414
static double get typeOptionItemHeight => 32 * scale;
1515
static double get typeOptionSeparatorHeight => 6 * scale;

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart';
22
import 'package:flowy_infra_ui/style_widget/hover.dart';
33
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
44
import 'package:flutter/widgets.dart';
5+
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
6+
import 'package:flowy_infra/theme.dart';
7+
import 'package:flutter/material.dart';
8+
import 'package:provider/provider.dart';
9+
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
10+
import 'package:styled_widget/styled_widget.dart';
511
import 'checkbox_cell.dart';
612
import 'date_cell.dart';
713
import 'number_cell.dart';
814
import 'selection_cell/selection_cell.dart';
915
import 'text_cell.dart';
1016

1117
GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) {
12-
final key = ValueKey(gridCell.rowId + gridCell.field.id);
18+
final key = ValueKey(gridCell.cellId());
1319

1420
final cellContextBuilder = GridCellContextBuilder(gridCell: gridCell, cellCache: cellCache);
1521

@@ -51,9 +57,157 @@ abstract class GridCellWidget extends HoverWidget {
5157
}
5258

5359
class GridCellRequestFocusNotifier extends ChangeNotifier {
60+
VoidCallback? _listener;
61+
62+
@override
63+
void addListener(VoidCallback listener) {
64+
if (_listener != null) {
65+
removeListener(_listener!);
66+
}
67+
68+
_listener = listener;
69+
super.addListener(listener);
70+
}
71+
72+
void removeAllListener() {
73+
if (_listener != null) {
74+
removeListener(_listener!);
75+
}
76+
}
77+
5478
void notify() {
5579
notifyListeners();
5680
}
5781
}
5882

5983
abstract class GridCellStyle {}
84+
85+
class CellSingleFocusNode extends FocusNode {
86+
VoidCallback? _listener;
87+
88+
void setSingleListener(VoidCallback listener) {
89+
if (_listener != null) {
90+
removeListener(_listener!);
91+
}
92+
93+
_listener = listener;
94+
super.addListener(listener);
95+
}
96+
97+
void removeSingleListener() {
98+
if (_listener != null) {
99+
removeListener(_listener!);
100+
}
101+
}
102+
}
103+
104+
class CellStateNotifier extends ChangeNotifier {
105+
bool _isFocus = false;
106+
bool _onEnter = false;
107+
108+
set isFocus(bool value) {
109+
if (_isFocus != value) {
110+
_isFocus = value;
111+
notifyListeners();
112+
}
113+
}
114+
115+
set onEnter(bool value) {
116+
if (_onEnter != value) {
117+
_onEnter = value;
118+
notifyListeners();
119+
}
120+
}
121+
122+
bool get isFocus => _isFocus;
123+
124+
bool get onEnter => _onEnter;
125+
}
126+
127+
class CellContainer extends StatelessWidget {
128+
final GridCellWidget child;
129+
final Widget? expander;
130+
final double width;
131+
final RegionStateNotifier rowStateNotifier;
132+
const CellContainer({
133+
Key? key,
134+
required this.child,
135+
required this.width,
136+
required this.rowStateNotifier,
137+
this.expander,
138+
}) : super(key: key);
139+
140+
@override
141+
Widget build(BuildContext context) {
142+
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
143+
create: (_) => CellStateNotifier(),
144+
update: (_, row, cell) => cell!..onEnter = row.onEnter,
145+
child: Selector<CellStateNotifier, bool>(
146+
selector: (context, notifier) => notifier.isFocus,
147+
builder: (context, isFocus, _) {
148+
Widget container = Center(child: child);
149+
child.onFocus.addListener(() {
150+
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
151+
});
152+
153+
if (expander != null) {
154+
container = _CellEnterRegion(child: container, expander: expander!);
155+
}
156+
157+
return GestureDetector(
158+
behavior: HitTestBehavior.translucent,
159+
onTap: () => child.requestFocus.notify(),
160+
child: Container(
161+
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
162+
decoration: _makeBoxDecoration(context, isFocus),
163+
padding: GridSize.cellContentInsets,
164+
child: container,
165+
),
166+
);
167+
},
168+
),
169+
);
170+
}
171+
172+
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
173+
final theme = context.watch<AppTheme>();
174+
if (isFocus) {
175+
final borderSide = BorderSide(color: theme.main1, width: 1.0);
176+
return BoxDecoration(border: Border.fromBorderSide(borderSide));
177+
} else {
178+
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
179+
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
180+
}
181+
}
182+
}
183+
184+
class _CellEnterRegion extends StatelessWidget {
185+
final Widget child;
186+
final Widget expander;
187+
const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
188+
189+
@override
190+
Widget build(BuildContext context) {
191+
return Selector<CellStateNotifier, bool>(
192+
selector: (context, notifier) => notifier.onEnter,
193+
builder: (context, onEnter, _) {
194+
List<Widget> children = [child];
195+
if (onEnter) {
196+
children.add(expander.positioned(right: 0));
197+
}
198+
199+
return MouseRegion(
200+
cursor: SystemMouseCursors.click,
201+
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
202+
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
203+
child: Stack(
204+
alignment: AlignmentDirectional.center,
205+
fit: StackFit.expand,
206+
// alignment: AlignmentDirectional.centerEnd,
207+
children: children,
208+
),
209+
);
210+
},
211+
);
212+
}
213+
}

0 commit comments

Comments
 (0)