我如何 select Widgets 通过拖拽它们,同时在 flutter 中单独点击它们?
How can I select Widgets by dragging over them but also clicking them individually in flutter?
我想创建一个可以在多个区域上拖动手指的界面。这会将区域的状态更改为选定状态(参见图片)。
解决这个问题的最佳方法是什么?
起始位置:
开始拖动:
Select 第一区:
Selected 所有地区:
当前 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);
}
我用的是矩形 class.
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class Whosebug extends StatefulWidget {
const Whosebug({Key? key}) : super(key: key);
@override
_WhosebugState createState() => _WhosebugState();
}
class _WhosebugState extends State<Whosebug> {
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
offers another related approach. From the code,你可以看到其他有趣的东西:
- 使用GestureDetector封装选区(这里是GridView)
- 在
GridView.itemBuilder
中,自定义ProxyWidget (his Selectable) wraps the normal widget builder for your selectable items. This is used to expose mount/unmount points in order to hang onto the corresponding custom ProxyElement。
- 当检测到 tap/motion 时,他使用当前上下文获取 RenderObject covering the selection area to do manual hit testing with the local position Offset, by checking if any of the cached Elements contain the point, using each item's bounding box in the selection area's coordinate system. (see _findIndexOfSelectable and Selectable.containsOffset) (这就像@mario 的回答,并且可能比@a.shak's 如果有很多可能的元素可供选择
屏幕。)
- 结果通过 ValueNotifier, which also lets the user control clearing or setting a custom the selection. (see the controller code)
返回给用户
为了对比,我会尝试用文字描述@a.shak的回答:
在他的 GridState
class 中,将 Listener 包裹在代表您的选择区域的子树周围。 (虽然 GestureDetector
也可以)
- 在
onPointerDown|Move
中,开始检测; onPointerUp
你可以 clear/etc.
- 检测需要获取子树的RenderBox (a
RenderObject
) so you can do hitTest
ing with the pointer’s local position to find other intersecting ROs. Given the selection area's RB, convert the pointer to its local coordinates and do the RenderBox.hitTest, then walk along the BoxHitTestResult.path of intersecting objects to check whether any HitTestEntry是我们知道可以选择的类型。 (即 _Foo extends RenderProxyBox
class - 见下文)
- 如果匹配成功!跟踪其信息以获取 UI 更新并稍后在其他地方使用。
使用 GlobalKey 和 GridView
来得到 RenderBox
对应于命中测试期间选择区域的范围。 (可能不需要这个,因为你可以使用国家自己的context
…)
在 GridView.itemBuilder
中,将您可选择的 object 包裹在自定义 SingleChildRenderObjectWidget 中,用于获取项目的 RenderBox
以进行命中测试和存储信息。
- 在此处存储信息,例如您的项目索引,并将其向下推送到我们的 SCROW 创建的自定义
RenderBox
中。
- 使用 RenderProxyBox 因为我们实际上并不关心控制渲染;只需将其全部委托给 child。这种自定义 class 还可以让我们在命中测试期间更轻松地找到我们感兴趣的可选 object(s)(参见
_detectTapedItem
)。
所以在这两种情况下,您都需要实现一些额外的自定义 classes (ProxyWidget
+ProxyElement
vs SingleChildRenderObjectWidget
+RenderProxyBox
)获得正确的 RenderBox
es 以使用屏幕上的选定点进行命中测试,并存储诸如项目索引之类的杂项信息以更新 UI 并在以后使用。
对于自定义形状,您可以让 CustomPainter override its hitTest
method to leverage Path.contains()
to restrict touches to be within the path only. See . Or just use a a package like touchable
为您的形状提供手势回调。
我想创建一个可以在多个区域上拖动手指的界面。这会将区域的状态更改为选定状态(参见图片)。
解决这个问题的最佳方法是什么?
起始位置:
开始拖动:
Select 第一区:
Selected 所有地区:
当前 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);
}
我用的是矩形 class.
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class Whosebug extends StatefulWidget {
const Whosebug({Key? key}) : super(key: key);
@override
_WhosebugState createState() => _WhosebugState();
}
class _WhosebugState extends State<Whosebug> {
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
offers another related approach. From the code,你可以看到其他有趣的东西:
- 使用GestureDetector封装选区(这里是GridView)
- 在
GridView.itemBuilder
中,自定义ProxyWidget (his Selectable) wraps the normal widget builder for your selectable items. This is used to expose mount/unmount points in order to hang onto the corresponding custom ProxyElement。 - 当检测到 tap/motion 时,他使用当前上下文获取 RenderObject covering the selection area to do manual hit testing with the local position Offset, by checking if any of the cached Elements contain the point, using each item's bounding box in the selection area's coordinate system. (see _findIndexOfSelectable and Selectable.containsOffset) (这就像@mario 的回答,并且可能比@a.shak's 如果有很多可能的元素可供选择 屏幕。)
- 结果通过 ValueNotifier, which also lets the user control clearing or setting a custom the selection. (see the controller code) 返回给用户
为了对比,我会尝试用文字描述@a.shak的回答:
在他的
GridState
class 中,将 Listener 包裹在代表您的选择区域的子树周围。 (虽然GestureDetector
也可以)- 在
onPointerDown|Move
中,开始检测;onPointerUp
你可以 clear/etc. - 检测需要获取子树的RenderBox (a
RenderObject
) so you can dohitTest
ing with the pointer’s local position to find other intersecting ROs. Given the selection area's RB, convert the pointer to its local coordinates and do the RenderBox.hitTest, then walk along the BoxHitTestResult.path of intersecting objects to check whether any HitTestEntry是我们知道可以选择的类型。 (即_Foo extends RenderProxyBox
class - 见下文)- 如果匹配成功!跟踪其信息以获取 UI 更新并稍后在其他地方使用。
- 在
使用 GlobalKey 和
GridView
来得到RenderBox
对应于命中测试期间选择区域的范围。 (可能不需要这个,因为你可以使用国家自己的context
…)在
GridView.itemBuilder
中,将您可选择的 object 包裹在自定义 SingleChildRenderObjectWidget 中,用于获取项目的RenderBox
以进行命中测试和存储信息。- 在此处存储信息,例如您的项目索引,并将其向下推送到我们的 SCROW 创建的自定义
RenderBox
中。 - 使用 RenderProxyBox 因为我们实际上并不关心控制渲染;只需将其全部委托给 child。这种自定义 class 还可以让我们在命中测试期间更轻松地找到我们感兴趣的可选 object(s)(参见
_detectTapedItem
)。
- 在此处存储信息,例如您的项目索引,并将其向下推送到我们的 SCROW 创建的自定义
所以在这两种情况下,您都需要实现一些额外的自定义 classes (ProxyWidget
+ProxyElement
vs SingleChildRenderObjectWidget
+RenderProxyBox
)获得正确的 RenderBox
es 以使用屏幕上的选定点进行命中测试,并存储诸如项目索引之类的杂项信息以更新 UI 并在以后使用。
对于自定义形状,您可以让 CustomPainter override its hitTest
method to leverage Path.contains()
to restrict touches to be within the path only. See touchable
为您的形状提供手势回调。