gpt4 book ai didi

flutter - 如何将焦点固定在 Flutter 中的 ListView 项目上?

转载 作者:行者123 更新时间:2023-12-05 05:39:12 29 4
gpt4 key购买 nike

我有一个 ListView ,我想启用 Ctrl+cEnter 等快捷方式,这可以改善用户体验。

enter image description here

问题是在我单击/点击某个项目后,它失去了焦点并且快捷键不再起作用。

是否有针对此问题的修复或解决方法?

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';

void main() {
runApp(const MyApp());
}

class SomeIntent extends Intent {}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: const MyHomePage(),
);
}
}

class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return GetBuilder<Controller>(
init: Get.put(Controller()),
builder: (controller) {
final List<MyItemModel> myItemModelList = controller.myItemModelList;
return Scaffold(
appBar: AppBar(
title: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (event) {
if (event.logicalKey.keyLabel == 'Arrow Down') {
FocusScope.of(context).nextFocus();
}
},
child: const TextField(
autofocus: true,
),
),
),
body: myItemModelList.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemBuilder: (context, index) {
final MyItemModel item = myItemModelList[index];
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): SomeIntent(),
},
child: Actions(
actions: {
SomeIntent: CallbackAction<SomeIntent>(
// this will not launch if I manually focus on the item and press enter
onInvoke: (intent) => print(
'SomeIntent action was launched for item ${item.name}'),
)
},
child: InkWell(
focusColor: Colors.blue,
onTap: () {
print('clicked item $index');
controller.toggleIsSelected(item);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: myItemModelList[index].isSelected
? Colors.green
: null,
height: 50,
child: ListTile(
title: Text(myItemModelList[index].name),
subtitle: Text(myItemModelList[index].detail),
),
),
),
),
),
);
},
itemCount: myItemModelList.length,
),
);
},
);
}
}

class Controller extends GetxController {
List<MyItemModel> myItemModelList = [];

@override
void onReady() {
myItemModelList = buildMyItemModelList(100);

update();

super.onReady();
}

List<MyItemModel> buildMyItemModelList(int count) {
return Iterable<MyItemModel>.generate(
count,
(index) {
return MyItemModel('$index - check debug console after pressing Enter.',
'$index - click me & press Enter... nothing happens\nfocus by pressing TAB/Arrow Keys and press Enter.');
},
).toList();
}

toggleIsSelected(MyItemModel item) {
for (var e in myItemModelList) {
if (e == item) {
e.isSelected = !e.isSelected;
}
}

update();
}
}

class MyItemModel {
final String name;
final String detail;
bool isSelected = false;

MyItemModel(this.name, this.detail);
}
  • 使用 Windows 10 和 flutter 3.0.1 进行测试
  • 使用Get状态管理器。

最佳答案

在 Flutter 中,ListViewGridView 包含许多 ListTile 小部件,您可能会注意到选择和焦点是分开的。我们还有 tap() 的问题,它理想地设置了选择和焦点 - 但默认情况下,点击不会影响焦点或选择。

ListTile selected属性官方demo https://api.flutter.dev/flutter/material/ListTile/selected.html展示了我们如何手动实现一个 selected ListTile 并获取 tap() 来更改选定的 ListTile。但这在同步焦点方面对我们没有任何帮助。

Note: As that demo shows, tracking the selected ListTile needs tobe done manualy, by having e.g. a selectedIndex variable, then setting theselected property of a ListTile to true if the index matches theselectedIndex.

这里有几个解决同步焦点问题的方法,在 ListView 中选择并点击。

方案一(已弃用,不推荐):

主要问题是访问焦点行为——默认情况下我们无权访问到每个 ListTile 的 FocusNode。

UPDATE: Actually it turns out that there is a way to access a focusnode, and thus allocating our own focusnodes is not necessary - see Solution 2 below. You use the Focus widget with a child: Builder(builder: (BuildContext context) then you can access the focusnode with FocusScope.of(context).focusedChild. I am leaving this first solution here for study, but recommend solution 2 instead.

但是通过为列表中的每个 ListTile 项分配一个焦点节点ListView,我们接着做。你看,通常一个 ListTile 项分配它自己的焦点节点,但这对我们不利,因为我们想从访问每个焦点节点外。所以我们自己分配焦点节点,传递给ListTile 项目,因为我们构建它们,这意味着 ListTile 不再需要自己分配一个 FocusNode - 注意:这不是 hack - 提供自定义ListTile API 支持 FocusNodes。我们现在可以访问每个 ListTile 项的 FocusNode 对象,以及

  • 调用它的 requestFocus()选择更改时的方法。
  • 我们也在 FocusNode 中监听焦点变化的对象,并在焦点出现时更新选择变化。

我们自己提供给每个 ListTile 的自定义焦点节点的好处是:

  1. 我们可以从 ListTile 小部件外部访问焦点节点。
  2. 我们可以使用焦点节点来请求焦点。
  3. 我们可以听到焦点的变化。
  4. 奖励:我们可以将快捷方式直接连接到焦点节点,而无需通常的 Flutter 快捷方式复杂性。

此代码同步选择、焦点和点击行为,并支持向上和向下箭头更改选择。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

// Enhancements to the official ListTile 'selection' demo
// https://api.flutter.dev/flutter/material/ListTile/selected.html to
// incorporate Andy's enhancements to sync tap, focus and selected.
// This version includes up/down arrow key support.

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});

static const String _title =
'Synchronising ListTile selection, focus and tap - with up/down arrow key support';

@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatefulWidget(),
),
);
}
}

class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});

@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
late List _focusNodes; // our custom focus nodes

void changeSelected(int index) {
setState(() {
_selectedIndex = index;
});
}

void changeFocus(int index) {
_focusNodes[index].requestFocus(); // this works!
}

// initstate
@override
void initState() {
super.initState();

_focusNodes = List.generate(
10,
(index) => FocusNode(onKeyEvent: (node, event) {
print(
'focusnode detected: ${event.logicalKey.keyLabel} ${event.runtimeType} $index ');
// The focus change that happens when the user presses TAB,
// SHIFT+TAB, UP and DOWN arrow keys happens on KeyDownEvent (not
// on the KeyUpEvent), so we ignore the KeyDownEvent and let
// Flutter do the focus change. That way we don't need to worry
// about programming manual focus change ourselves, say, via
// methods on the focus nodes, which would be an unecessary
// duplication.
//
// Once the focus change has happened naturally, all we need to do
// is to change our selected state variable (which we are manually
// managing) to the new item position (where the focus is now) -
// we can do this in the KeyUpEvent. The index of the KeyUpEvent
// event will be item we just moved focus to (the KeyDownEvent
// supplies the old item index and luckily the corresponding
// KeyUpEvent supplies the new item index - where the focus has
// just moved to), so we simply set the selected state value to
// that index.

if (event.runtimeType == KeyUpEvent &&
(event.logicalKey == LogicalKeyboardKey.arrowUp ||
event.logicalKey == LogicalKeyboardKey.arrowDown ||
event.logicalKey == LogicalKeyboardKey.tab)) {
changeSelected(index);
}

return KeyEventResult.ignored;
}));
}

@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return ListTile(
focusNode: _focusNodes[
index], // allocate our custom focus node for each item
title: Text('Item $index'),
selected: index == _selectedIndex,
onTap: () {
changeSelected(index);
changeFocus(index);
},
);
},
);
}
}

重要说明:上述解决方案在更改项目数量时不起作用,因为所有焦点节点都是在 initState 期间分配的,它只被调用一次。例如,如果项目数量增加,则没有足够的焦点节点可以绕过,构建步骤将崩溃。

下一个解决方案(如下)没有显式分配焦点节点,是一个更强大的解决方案,支持动态重建和添加和删除项目。

方案二(允许重建,推荐)

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:developer' as developer;

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});

static const String _title = 'Flutter selectable listview - solution 2';

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: HomeWidget(),
);
}
}

// ╦ ╦┌─┐┌┬┐┌─┐╦ ╦┬┌┬┐┌─┐┌─┐┌┬┐
// ╠═╣│ ││││├┤ ║║║│ │││ ┬├┤ │
// ╩ ╩└─┘┴ ┴└─┘╚╩╝┴─┴┘└─┘└─┘ ┴

class HomeWidget extends StatefulWidget {
const HomeWidget({super.key});

@override
State<HomeWidget> createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
// generate a list of 10 string items
List<String> _items = List<String>.generate(10, (int index) => 'Item $index');
String currentItem = '';
int currentIndex = 0;
int redrawTrigger = 0;

// clear items method inside setstate
void _clearItems() {
setState(() {
currentItem = '';
_items.clear();
});
}

// add items method inside setstate
void _rebuildItems() {
setState(() {
currentItem = '';
_items.clear();
_items.addAll(List<String>.generate(5, (int index) => 'Item $index'));
});
}

// set currentItem method inside setstate
void _setCurrentItem(String item) {
setState(() {
currentItem = item;
currentIndex = _items.indexOf(item);
});
}

// set currentindex method inside setstate
void _setCurrentIndex(int index) {
setState(() {
currentIndex = index;
if (index < 0 || index >= _items.length) {
currentItem = '';
} else {
currentItem = _items[index];
}
});
}

// delete current index method inside setstate
void _deleteCurrentIndex() {
// ensure that the index is valid
if (currentIndex >= 0 && currentIndex < _items.length) {
setState(() {
String removedValue = _items.removeAt(currentIndex);
if (removedValue.isNotEmpty) {
print('Item index $currentIndex deleted, which was $removedValue');

// calculate new focused index, if have deleted the last item
int newFocusedIndex = currentIndex;
if (newFocusedIndex >= _items.length) {
newFocusedIndex = _items.length - 1;
}
_setCurrentIndex(newFocusedIndex);
print('setting new newFocusedIndex to $newFocusedIndex');
} else {
print('Failed to remove $currentIndex');
}
});
} else {
print('Index $currentIndex is out of range');
}
}

@override
Widget build(BuildContext context) {
// print the current time
print('HomeView build at ${DateTime.now()} $_items');
return Scaffold(
body: Column(
children: [
// display currentItem
Text(currentItem),
Text(currentIndex.toString()),
ElevatedButton(
child: Text("Force Draw"),
onPressed: () => setState(() {
redrawTrigger = redrawTrigger + 1;
}),
),
ElevatedButton(
onPressed: () {
_setCurrentItem('Item 0');
redrawTrigger = redrawTrigger + 1;
},
child: const Text('Set to Item 0'),
),
ElevatedButton(
onPressed: () {
_setCurrentIndex(1);
redrawTrigger = redrawTrigger + 1;
},
child: const Text('Set to index 1'),
),
// button to clear items
ElevatedButton(
onPressed: _clearItems,
child: const Text('Clear Items'),
),
// button to add items
ElevatedButton(
onPressed: _rebuildItems,
child: const Text('Rebuild Items'),
),
// button to delete current item
ElevatedButton(
onPressed: _deleteCurrentIndex,
child: const Text('Delete Current Item'),
),
Expanded(
key: ValueKey('${_items.length} $redrawTrigger'),
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
// print(' building listview index $index');
return FocusableText(
_items[index],
autofocus: index == currentIndex,
updateCurrentItemParentCallback: _setCurrentItem,
deleteCurrentItemParentCallback: _deleteCurrentIndex,
);
},
itemCount: _items.length,
),
),
],
),
);
}
}

// ╔═╗┌─┐┌─┐┬ ┬┌─┐┌─┐┌┐ ┬ ┌─┐╔╦╗┌─┐─┐ ┬┌┬┐
// ╠╣ │ ││ │ │└─┐├─┤├┴┐│ ├┤ ║ ├┤ ┌┴┬┘ │
// ╚ └─┘└─┘└─┘└─┘┴ ┴└─┘┴─┘└─┘ ╩ └─┘┴ └─ ┴

class FocusableText extends StatelessWidget {
const FocusableText(
this.data, {
super.key,
required this.autofocus,
required this.updateCurrentItemParentCallback,
required this.deleteCurrentItemParentCallback,
});

/// The string to display as the text for this widget.
final String data;

/// Whether or not to focus this widget initially if nothing else is focused.
final bool autofocus;

final updateCurrentItemParentCallback;
final deleteCurrentItemParentCallback;

@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.keyX): () {
print('X pressed - attempting to delete $data');
deleteCurrentItemParentCallback();
},
},
child: Focus(
autofocus: autofocus,
onFocusChange: (value) {
print(
'$data onFocusChange ${FocusScope.of(context).focusedChild}: $value');
if (value) {
updateCurrentItemParentCallback(data);
}
},
child: Builder(builder: (BuildContext context) {
// The contents of this Builder are being made focusable. It is inside
// of a Builder because the builder provides the correct context
// variable for Focus.of() to be able to find the Focus widget that is
// the Builder's parent. Without the builder, the context variable used
// would be the one given the FocusableText build function, and that
// would start looking for a Focus widget ancestor of the FocusableText
// instead of finding the one inside of its build function.
developer.log('build $data', name: '${Focus.of(context)}');
return GestureDetector(
onTap: () {
Focus.of(context).requestFocus();
// don't call updateParentCallback('data') here, it will be called by onFocusChange
},
child: ListTile(
leading: Icon(Icons.map),
selectedColor: Colors.red,
selected: Focus.of(context).hasPrimaryFocus,
title: Text(data),
),
);
}),
),
);
}
}

关于flutter - 如何将焦点固定在 Flutter 中的 ListView 项目上?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72757996/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com