Flutter 滚动视图到列上的焦点小部件

Posted

技术标签:

【中文标题】Flutter 滚动视图到列上的焦点小部件【英文标题】:Flutter Scroll view to focused widget on a column 【发布时间】:2021-11-10 07:25:24 【问题描述】:

我正在为 android TV 开发一个应用,并使用 DPAD 导航。 我在一列中有多个小部件。当我导航到视图之外的小部件时,小部件/视图不会移动以反映所选小部件。

// ignore_for_file: avoid_print

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

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

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

  static const String _title = 'Flutter Code Sample';

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


class MyStatelessWidget extends StatelessWidget 
  const MyStatelessWidget(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    final TextTheme textTheme = Theme.of(context).textTheme;
    return DefaultTextStyle(
      style: textTheme.headline4!,
      child: ChangeNotifierProvider<SampleNotifier>(
          create: (context) => SampleNotifier(), child: const CardHolder()),
    );
  


class CardHolder extends StatefulWidget 
  const CardHolder(Key? key) : super(key: key);

  @override
  _CardHolderState createState() => _CardHolderState();


class _CardHolderState extends State<CardHolder> 
  late FocusNode _focusNode;
  late FocusAttachment _focusAttachment;

  @override
  void initState() 
    super.initState();
    _focusNode = FocusNode(debugLabel: "traversal_node");
    _focusAttachment = _focusNode.attach(context, onKey: _handleKeyPress);
    _focusNode.requestFocus();
  

  @override
  Widget build(BuildContext context) 
    _focusAttachment.reparent();
    return Focus(
      focusNode: _focusNode,
      autofocus: true,
      onKey: _handleKeyPress,
      child: Consumer<SampleNotifier>(
        builder: (context, models, child) 
          int listSize = Provider.of<SampleNotifier>(context).listSize;
          return SingleChildScrollView(
            child: SampleRow(cat: "Test", models: models.modelList),
          );
        ,
      ),
    );
  

  KeyEventResult _handleKeyPress(FocusNode node, RawKeyEvent event) 
    if (event is RawKeyDownEvent) 
      print("t:FocusNode: $node.debugLabel event: $event.logicalKey");
      if (event.logicalKey == LogicalKeyboardKey.arrowRight) 
        Provider.of<SampleNotifier>(context, listen: false).moveRight();
        return KeyEventResult.handled;
       else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) 
        Provider.of<SampleNotifier>(context, listen: false).moveLeft();
        return KeyEventResult.handled;
      
    
    // debugDumpFocusTree();
    return KeyEventResult.ignored;
  


class SampleCard extends StatefulWidget 
  final int number;
  final SampleModel model;
  final bool focused;
  const SampleCard(
      required this.number,
      required this.focused,
      required this.model,
      Key? key)
      : super(key: key);

  @override
  _SampleCardState createState() => _SampleCardState();


class _SampleCardState extends State<SampleCard> 
  late Color _color;

  @override
  void initState() 
    super.initState();
    _color = Colors.red.shade900;
  

  @override
  void dispose() 
    super.dispose();
  

  @override
  Widget build(BuildContext context) 
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 10),
      child: widget.focused
          ? Container(
              width: 150,
              height: 300,
              color: Colors.white,
              child: Center(
                child: Text(
                  "$widget.model.text $widget.model.num",
                  style: TextStyle(color: _color),
                ),
              ),
            )
          : Container(
              width: 150,
              height: 300,
              color: Colors.black,
              child: Center(
                child: Text(
                  "$widget.model.text $widget.model.num",
                  style: TextStyle(color: _color),
                ),
              ),
            ),
    );
  


class SampleRow extends StatelessWidget 
  final String cat;
  final List<SampleModel> models;

  SampleRow(Key? key, required this.cat, required this.models) : super(key: key);

  @override
  Widget build(BuildContext context) 
    final int selectedIndex =
        Provider.of<SampleNotifier>(context).selectedIndex;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Padding(
          padding: EdgeInsets.only(left: 16, bottom: 8),
        ),
        models.isNotEmpty
            ? SizedBox(
                height: 200,
                child: ListView.custom(
                  padding: const EdgeInsets.all(8),
                  scrollDirection: Axis.horizontal,
                  childrenDelegate: SliverChildBuilderDelegate(
                    (context, index) => Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 8),
                        child: SampleCard(
                          focused: index == selectedIndex,
                          model: models[index],
                          number: index,
                        ),
                      ),
                    childCount: models.length,
                    findChildIndexCallback: _findChildIndex,
                  ),
                ),
              )
            : SizedBox(
                height: 200,
                child: Container(
                  color: Colors.teal,
                ),
              )
      ],
    );
  

  int _findChildIndex(Key key) => models.indexWhere((model) =>
      "$cat-$model.text_$model.num" == (key as ValueKey<String>).value);


class SampleNotifier extends ChangeNotifier 
  final List<SampleModel> _models = [
    SampleModel(0, "zero"),
    SampleModel(1, "one"),
    SampleModel(2, "two"),
    SampleModel(3, "three"),
    SampleModel(4, "four"),
    SampleModel(5, "five"),
    SampleModel(6, "six"),
    SampleModel(7, "seven"),
    SampleModel(8, "eight"),
    SampleModel(9, "nine"),
    SampleModel(10, "ten")
  ];

  int _selectedIndex = 0;

  List<SampleModel> get modelList => _models;

  int get selectedIndex => _selectedIndex;

  int get listSize => _models.length;

  void moveRight() 
    if (_selectedIndex < _models.length - 1) 
      _selectedIndex = _selectedIndex + 1;
    
    notifyListeners();
  

  void moveLeft() 
    if (_selectedIndex > 0) 
      _selectedIndex = _selectedIndex - 1;
    
    notifyListeners();
  


class SampleModel 
  int num;
  String text;

  SampleModel(this.num, this.text);

我需要一种将小部件移动/滚动到视图中的方法。有什么办法可以做到这一点,在 android tv 上使用 DPAD 导航

Here is the gist

【问题讨论】:

【参考方案1】:

您可以使用scrollable_positioned_list 包。

这个小部件不是基于像素滚动的ListView.custom,而是基于索引:

final ItemScrollController itemScrollController = ItemScrollController();

ScrollablePositionedList.builder(
  itemCount: 500,
  itemBuilder: (context, index) => Text('Item $index'),
  itemScrollController: itemScrollController,
  itemPositionsListener: itemPositionsListener,
);

因此,您可以保持当前滚动位置的索引,并在 DPAD 上按下:

itemScrollController.jumpTo(index: currentItem);
setState(()currentItem++;)

【讨论】:

以上是关于Flutter 滚动视图到列上的焦点小部件的主要内容,如果未能解决你的问题,请参考以下文章

如何暗示颤振小部件是可滚动的?

Flutter ListView 滚动动画重叠兄弟小部件

在 Flutter 中保持滚动视图偏移的同时添加列表视图项

Flutter ListView.Builder() 在可滚动列中与其他小部件

PyQt5 - CSV导入,显示和滚动建议 - 视图与小部件,QTreeView与其他

将多个小部件添加到滚动视图