diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index d13f705dbec..3a62bb84992 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -206,6 +206,10 @@ class DataCell { this.placeholder = false, this.showEditIcon = false, this.onTap, + this.onLongPress, + this.onTapDown, + this.onDoubleTap, + this.onTapCancel, }) : assert(child != null); /// A cell that has no content and has zero width and height. @@ -241,14 +245,48 @@ class DataCell { /// Called if the cell is tapped. /// /// If non-null, tapping the cell will call this callback. If - /// null, tapping the cell will attempt to select the row (if + /// null (including [onDoubleTap], [onLongPress], [onTapCancel] and [onTapDown]), + /// tapping the cell will attempt to select the row (if /// [DataRow.onSelectChanged] is provided). - /// - /// To define a tap behavior for the entire row, see - /// [DataRow.onSelectChanged]. - final VoidCallback? onTap; + final GestureTapCallback? onTap; - bool get _debugInteractive => onTap != null; + /// Called when the cell is double tapped. + /// + /// If non-null, tapping the cell will call this callback. If + /// null (including [onTap], [onLongPress], [onTapCancel] and [onTapDown]), + /// tapping the cell will attempt to select the row (if + /// [DataRow.onSelectChanged] is provided). + final GestureTapCallback? onDoubleTap; + + /// Called if the cell is long-pressed. + /// + /// If non-null, tapping the cell will invoke this callback. If + /// null (including [onDoubleTap], [onTap], [onTapCancel] and [onTapDown]), + /// tapping the cell will attempt to select the row (if + /// [DataRow.onSelectChanged] is provided). + final GestureLongPressCallback? onLongPress; + + /// Called if the cell is tapped down. + /// + /// If non-null, tapping the cell will call this callback. If + /// null (including [onTap] [onDoubleTap], [onLongPress] and [onTapCancel]), + /// tapping the cell will attempt to select the row (if + /// [DataRow.onSelectChanged] is provided). + final GestureTapDownCallback? onTapDown; + + /// Called if the user cancels a tap was started on cell. + /// + /// If non-null, cancelling the tap gesture will invoke this callback. + /// If null (including [onTap], [onDoubleTap] and [onLongPress]), + /// tapping the cell will attempt to select the + /// row (if [DataRow.onSelectChanged] is provided). + final GestureTapCancelCallback? onTapCancel; + + bool get _debugInteractive => onTap != null || + onDoubleTap != null || + onLongPress != null || + onTapDown != null || + onTapCancel != null; } /// A material design data table. @@ -809,8 +847,12 @@ class DataTable extends StatelessWidget { required bool numeric, required bool placeholder, required bool showEditIcon, - required VoidCallback? onTap, + required GestureTapCallback? onTap, required VoidCallback? onSelectChanged, + required GestureTapCallback? onDoubleTap, + required GestureLongPressCallback? onLongPress, + required GestureTapDownCallback? onTapDown, + required GestureTapCancelCallback? onTapCancel, required MaterialStateProperty? overlayColor, }) { final ThemeData themeData = Theme.of(context); @@ -840,9 +882,17 @@ class DataTable extends StatelessWidget { child: DropdownButtonHideUnderline(child: label), ), ); - if (onTap != null) { + if (onTap != null || + onDoubleTap != null || + onLongPress != null || + onTapDown != null || + onTapCancel != null) { label = InkWell( onTap: onTap, + onDoubleTap: onDoubleTap, + onLongPress: onLongPress, + onTapCancel: onTapCancel, + onTapDown: onTapDown, child: label, overlayColor: overlayColor, ); @@ -1000,6 +1050,10 @@ class DataTable extends StatelessWidget { placeholder: cell.placeholder, showEditIcon: cell.showEditIcon, onTap: cell.onTap, + onDoubleTap: cell.onDoubleTap, + onLongPress: cell.onLongPress, + onTapCancel: cell.onTapCancel, + onTapDown: cell.onTapDown, onSelectChanged: () => row.onSelectChanged != null ? row.onSelectChanged!(!row.selected) : null, overlayColor: row.color ?? effectiveDataRowColor, ); diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 43f4d6b1106..961f5bc92eb 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -54,6 +54,18 @@ void main() { onTap: () { log.add('cell-tap: ${dessert.calories}'); }, + onDoubleTap: () { + log.add('cell-doubleTap: ${dessert.calories}'); + }, + onLongPress: () { + log.add('cell-longPress: ${dessert.calories}'); + }, + onTapCancel: () { + log.add('cell-tapCancel: ${dessert.calories}'); + }, + onTapDown: (TapDownDetails details) { + log.add('cell-tapDown: ${dessert.calories}'); + }, ), ], ); @@ -94,9 +106,41 @@ void main() { )); await tester.pumpAndSettle(const Duration(milliseconds: 200)); + await tester.tap(find.text('375')); + await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.text('375')); - expect(log, ['cell-tap: 375']); + expect(log, ['cell-doubleTap: 375']); + log.clear(); + + await tester.longPress(find.text('375')); + // The tap down is triggered on gesture down. + // Then, the cancel is triggered when the gesture arena + // recognizes that the long press overrides the tap event + // so it triggers a tap cancel, followed by the long press. + expect(log,['cell-tapDown: 375' ,'cell-tapCancel: 375', 'cell-longPress: 375']); + log.clear(); + + TestGesture gesture = await tester.startGesture( + tester.getRect(find.text('375')).center, + ); + await tester.pump(const Duration(milliseconds: 100)); + // onTapDown callback is registered. + expect(log, equals(['cell-tapDown: 375'])); + await gesture.up(); + + await tester.pump(const Duration(seconds: 1)); + // onTap callback is registered after the gesture is removed. + expect(log, equals(['cell-tapDown: 375', 'cell-tap: 375'])); + log.clear(); + + // dragging off the bounds of the cell calls the cancel callback + gesture = await tester.startGesture(tester.getRect(find.text('375')).center); + await tester.pump(const Duration(milliseconds: 100)); + await gesture.moveBy(const Offset(0.0, 200.0)); + await gesture.cancel(); + expect(log, equals(['cell-tapDown: 375', 'cell-tapCancel: 375'])); + log.clear(); await tester.tap(find.byType(Checkbox).last);