这段代码需要更新以适应当前的Flutter/Dart版本,但这份代码对我有用。
更新后的代码:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Grid(),
);
}
}
class Grid extends StatefulWidget {
@override
GridState createState() {
return new GridState();
}
}
class GridState extends State<Grid> {
final Set<int> selectedIndexes = Set<int>();
final key = GlobalKey();
final Set<_Foo> _trackTaped = Set<_Foo>();
_detectTapedItem(PointerEvent event) {
final RenderBox box = key.currentContext!.findAncestorRenderObjectOfType<RenderBox>()!;
final result = BoxHitTestResult();
Offset local = box.globalToLocal(event.position);
if (box.hitTest(result, position: local)) {
for (final hit in result.path) {
/// temporary variable so that the [is] allows access of [index]
final target = hit.target;
if (target is _Foo && !_trackTaped.contains(target)) {
_trackTaped.add(target);
_selectIndex(target.index);
}
}
}
}
_selectIndex(int index) {
setState(() {
selectedIndexes.add(index);
});
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _detectTapedItem,
onPointerMove: _detectTapedItem,
onPointerUp: _clearSelection,
child: GridView.builder(
key: key,
itemCount: 6,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemBuilder: (context, index) {
return Foo(
index: index,
child: Container(
color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
),
);
},
),
);
}
void _clearSelection(PointerUpEvent event) {
_trackTaped.clear();
setState(() {
selectedIndexes.clear();
});
}
}
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({required Widget child, required this.index, Key? key}) : super(child: child, key: key);
@override
_Foo createRenderObject(BuildContext context) {
return _Foo(index);
}
@override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
_Foo(this.index);
}
我使用 Rect 类。
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class StackOverflow extends StatefulWidget {
const StackOverflow({Key? key}) : super(key: key);
@override
_StackOverflowState createState() => _StackOverflowState();
}
class _StackOverflowState extends State<StackOverflow> {
late List<bool> isSelected;
late List<GlobalKey> myGlobalKey;
late List<Offset> offsetWidgets;
late List<Size> sizeWidgets;
late List<Rect> listRect;
@override
void initState() {
super.initState();
isSelected = List.generate(3, (index) => false);
myGlobalKey = List.generate(3, (index) => GlobalKey());
offsetWidgets = <Offset>[];
sizeWidgets = <Size>[];
listRect = <Rect>[];
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
for (final key in myGlobalKey) {
sizeWidgets
.add((key.currentContext!.findRenderObject() as RenderBox).size);
offsetWidgets.add((key.currentContext!.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero));
}
for (int i = 0; i < 3; i++) {
final dx = offsetWidgets[i].dx + sizeWidgets[i].width;
final dy = offsetWidgets[i].dy + sizeWidgets[i].height;
listRect.add(Rect.fromPoints(offsetWidgets[i], Offset(dx, dy)));
}
});
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
if (listRect[0].contains(pointerMoveEvent.position)) {
if (!isSelected[0]) {
setState(() {
isSelected[0] = true;
});
}
} else if (listRect[1].contains(pointerMoveEvent.position)) {
if (!isSelected[1]) {
setState(() {
isSelected[1] = true;
});
}
} else if (listRect[2].contains(pointerMoveEvent.position)) {
if (!isSelected[2]) {
setState(() {
isSelected[2] = true;
});
}
}
},
child: Container(
color: Colors.amber,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RawMaterialButton(
key: myGlobalKey[0],
fillColor: isSelected[0] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[0] = false;
});
},
),
RawMaterialButton(
key: myGlobalKey[1],
fillColor: isSelected[1] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[1] = false;
});
},
),
RawMaterialButton(
key: myGlobalKey[2],
fillColor: isSelected[2] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[2] = false;
});
},
),
],
),
),
);
}
}
drag_select_grid_view
提供了另一种相关的方法。从代码中可以看到:其中包括以下有趣的事情:
GridView.itemBuilder
中,自定义ProxyWidget(他的Selectable)包装可选择项的常规小部件生成器。这用于公开挂载/卸载点,以便悬挂相应的自定义ProxyElement。为了对比,我将试图用言语描述@a.shak的答案:
在你的GridState
类中,使用一个Listener包裹代表选择区域的子树。 (虽然GestureDetector
也可以)
onPointerDown|Move
中开始检测;在onPointerUp
中可以清除等操作。RenderObject
),以便使用指针的本地位置进行hitTest
以查找其他相交的ROs。给定选择区域的RB,请将指针转换为其本地坐标并进行RenderBox.hitTest,然后沿着相交对象的BoxHitTestResult.path行走,检查任何HitTestEntry是否属于我们知道可被选择的类型(即_Foo extends RenderProxyBox
类 - 请参见下文)
使用GlobalKey与GridView
一起使用,以获取在命中测试期间对应于选择区域的范围的RenderBox
。 (可能不需要这个,因为可以使用状态自己的context
...)
在GridView.itemBuilder
中,将可选择的对象包装在自定义的SingleChildRenderObjectWidget中,用于获取项目的RenderBox
以进行命中测试和存储信息。
RenderBox
中。_detectTapedItem
)。ProxyWidget
+ProxyElement
或SingleChildRenderObjectWidget
+RenderProxyBox
),以便使用屏幕上选择的点进行命中测试并获取正确的RenderBox
,并存储杂项信息,如项目的索引以更新UI并稍后使用。hitTest
方法,利用Path.contains()
来限制触摸仅在路径内。请参见此答案。或者只需使用像touchable
这样的包,为您的形状提供手势回调。DragRegion
和DragRegionTarget
。请参考每个组件的使用说明。import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Instructions:
/// 1. Wrap the area you want to be in your drag region with [DragRegion]
/// 2. Wrap each element you want to listen to the drag events with [DragRegionTarget]
class DragRegion<T> extends StatefulWidget {
final Widget child;
/// Called on the first time the drag enters the target since the drag started
final void Function(ValueKey<T> valueKey)? onDragFirstEnter;
/// Called on every time the drag enters the target
final void Function(ValueKey<T> valueKey)? onDragEnter;
/// Called on every time the drag moves over the target
final void Function(ValueKey<T> valueKey)? onDragMove;
/// Called on every time the drag leaves the target
final void Function(ValueKey<T> valueKey)? onDragExit;
/// Called on the initial drag event
final void Function()? onDragStart;
/// Called on the ending drag event
final void Function()? onDragEnd;
const DragRegion({
super.key,
required this.child,
this.onDragFirstEnter,
this.onDragEnter,
this.onDragExit,
this.onDragMove,
this.onDragStart,
this.onDragEnd,
});
@override
State<DragRegion> createState() => _DragRegionState<T>();
}
class _DragRegionState<T> extends State<DragRegion<T>> {
late Set<ValueKey<T>> touchedValueKeys; // items that have been hit since drag start
late Set<ValueKey<T>> touchingValueKeys; // items that are currently being hit
late GlobalKey key; // Global key to identify the container
@override
void initState() {
touchedValueKeys = {};
touchingValueKeys = {};
key = GlobalKey();
super.initState();
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: detectTappedItem,
onPointerMove: detectTappedItem,
onPointerUp: (_) {
clearTappedItems();
widget.onDragEnd?.call();
},
onPointerCancel: (_) {
clearTappedItems();
widget.onDragEnd?.call();
},
child: Container(
key: key,
color: const Color(0x00000000), // remove this if you want the listener to ignore areas there aren't widgets in the child being displayed
child: widget.child,
),
);
}
void clearTappedItems() {
for (final valueKey in touchingValueKeys) {
widget.onDragExit?.call(valueKey);
}
touchingValueKeys.clear();
touchedValueKeys.clear();
}
void detectTappedItem(PointerEvent event) {
final box = key.currentContext?.findAncestorRenderObjectOfType<RenderBox>();
if (box == null) return;
final hitTestResult = BoxHitTestResult();
final local = box.globalToLocal(event.position);
if (!box.hitTest(hitTestResult, position: local)) return;
final Set<ValueKey<T>> currentlyTouchingValueKeys = {};
for (final hit in hitTestResult.path) {
final target = hit.target;
if (target is! _ValueKeyHolder<T>) continue;
final valueKey = target.valueKey;
currentlyTouchingValueKeys.add(valueKey);
if (!touchedValueKeys.contains(valueKey)) {
touchedValueKeys.add(valueKey);
widget.onDragFirstEnter?.call(valueKey);
}
}
// find out which ones have entered, stayed, and exited
final exitedValueKeys = touchingValueKeys.difference(currentlyTouchingValueKeys);
final enteredValueKeys = currentlyTouchingValueKeys.difference(touchingValueKeys);
for (final valueKey in enteredValueKeys) {
widget.onDragEnter?.call(valueKey);
}
for (final valueKey in currentlyTouchingValueKeys) {
widget.onDragMove?.call(valueKey);
}
for (final valueKey in exitedValueKeys) {
widget.onDragExit?.call(valueKey);
}
touchingValueKeys.clear();
touchingValueKeys.addAll(currentlyTouchingValueKeys);
}
}
///
/// Instructions:
/// 1. Identify each child with a unique ValueKey which will be passed to [DragRegion]'s callbacks
///
class DragRegionTarget<T> extends SingleChildRenderObjectWidget {
final ValueKey<T> valueKey;
const DragRegionTarget({required Widget child, required this.valueKey, Key? key}) : super(child: child, key: key);
@override
_ValueKeyHolder createRenderObject(BuildContext context) {
return _ValueKeyHolder<T>(valueKey);
}
@override
void updateRenderObject(BuildContext context, _ValueKeyHolder<T> renderObject) {
renderObject.valueKey = valueKey;
}
}
///
/// A class for holding the ValueKey
///
class _ValueKeyHolder<T> extends RenderProxyBox {
ValueKey<T> valueKey;
_ValueKeyHolder(this.valueKey);
}
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Grid(),
);
}
}
class Grid extends StatefulWidget {
@override
GridState createState() {
return GridState();
}
}
class GridState extends State<Grid> {
final Set<int> selectedIndexes = {};
_selectIndex(int index) {
setState(() {
selectedIndexes.add(index);
});
}
void _clearSelection() {
setState(() {
selectedIndexes.clear();
});
}
@override
Widget build(BuildContext context) {
return DragRegion<int>(
onDragFirstEnter: (valueKey) {
_selectIndex(valueKey.value);
},
onDragEnd: () {
_clearSelection();
},
child: GridView.builder(
itemCount: 6,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemBuilder: (context, index) {
return DragRegionTarget<int>(
valueKey: ValueKey(index),
child: Container(
color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
),
);
},
),
);
}
}