Flutter 滚动视图以将小部件集中在列上

Flutter Scroll view to focused widget on a column

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

// 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);
}

我需要一种方法来 move/scroll 小部件进入视图。有什么方法可以做到这一点,在 android tv

上使用 DPAD 导航

Here is the gist

您可以使用 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++;})