Нажмите «Виджет» за пределами стека во Flutter

В настоящее время существует известное ограничение Flutter, из-за которого виджеты выходят за пределы Stack и не получают событий жестов.

Однако есть хак , который, похоже, работает, расширяя класс Stack.

Вот моя обновленная реализация взлома.

class StackTappableOutside extends Stack {
  const StackTappableOutside({
    super.key,
    super.alignment,
    super.textDirection,
    super.fit,
    super.clipBehavior,
    super.children,
  });

  @override
  RenderStack createRenderObject(BuildContext context) {
    return RenderStackTappableOutside(
      alignment: alignment,
      textDirection: textDirection ?? Directionality.of(context),
      fit: fit,
      clipBehavior: clipBehavior,
    );
  }
}

class RenderStackTappableOutside extends RenderStack {
  RenderStackTappableOutside({
    super.alignment,
    super.textDirection,
    super.fit,
    super.clipBehavior,
    super.children,
  });

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    return false;
  }
}

Я протестировал его, и он хорошо работает в Column и Row.

(Воспроизводимо, просто скопируйте и вставьте)

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class StackTappableOutside extends Stack {
  const StackTappableOutside({
    super.key,
    super.alignment,
    super.textDirection,
    super.fit,
    super.clipBehavior,
    super.children,
  });

  @override
  RenderStack createRenderObject(BuildContext context) {
    return RenderStackTappableOutside(
      alignment: alignment,
      textDirection: textDirection ?? Directionality.of(context),
      fit: fit,
      clipBehavior: clipBehavior,
    );
  }
}

class RenderStackTappableOutside extends RenderStack {
  RenderStackTappableOutside({
    super.alignment,
    super.textDirection,
    super.fit,
    super.clipBehavior,
    super.children,
  });

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    return false;
  }
}

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

  @override
  State<TapOutsideStackDemo> createState() => _TapOutsideStackDemoState();
}

class _TapOutsideStackDemoState extends State<TapOutsideStackDemo> {
  List<int> items = [0, 1, 2, 3, 4];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Tap Outside Stack Demo'),
      ),
      body: _body(),
    );
  }

  Widget _itemBuilder(int index, int element) {
    final showAddButton = index > 0;
    return StackTappableOutside(
      clipBehavior: Clip.none,
      children: [
        Container(
          child: ListTile(
            title: Text('Todo List Item $element'),
            subtitle: Text('Add a new item after'),
          ),
          decoration: BoxDecoration(
            color: Colors.deepOrange,
            border: Border.all(color: Colors.yellow),
          ),
        ),
        if (showAddButton)
          Positioned(
            top: -24,
            right: 8,
            child: Container(
              clipBehavior: Clip.antiAlias,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: Colors.green,
              ),
              child: IconButton(
                icon: Icon(Icons.add),
                onPressed: () {
                  print('add after');
                },
              ),
            ),
          ),
      ],
    );
  }

  Widget _body() {
    return Column(
      children: items.mapIndexed(_itemBuilder).toList(),
    );
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        final element = items[index];
        return _itemBuilder(index, element);
      },
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: TapOutsideStackDemo(),
  ));
}

С помощью приведенного выше кода, когда вы нажимаете зеленую кнопку из любого места, печатается:

add after
add after
add after

Однако у меня есть требование сделать это ListView вместо Column. Поэтому я изменился с

  Widget _body() {
    return Column(
      children: items.mapIndexed(_itemBuilder).toList(),
    );
  }

к:

  Widget _body() {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        final element = items[index];
        return _itemBuilder(index, element);
      },
    );
  }

Внезапно работает только половина зеленой кнопки. Работает только нижняя часть.

Я также заметил, что если мы обернем элемент списка GestureDetector, хак тоже перестанет работать, даже с простым Column, который работал раньше.

  • Может кто-нибудь объяснить, почему это не работает в ListView?

  • Могу ли я как-нибудь заставить это работать на ListView и GestureDetector? Мне нужно использовать ListView обязательно, потому что мне нужно будет реализовать ReorderableListView.builder потом.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
52
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Кажется довольно странным, что не существует простого и готового способа сделать это. Но я нашел хороший пакет для пабов, который, похоже, мне подходит: https://pub.dev/packages/defer_pointer

В вашем примере это сработало, обернув весь ListView.builder внутри DeferredPointerHandler и завернув IconButton внутрь DeferPointer.

Вот ваш измененный пример кода:

class _TapOutsideStackDemoState extends State<TapOutsideStackDemo> {
  List<int> items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Tap Outside Stack Demo'),
      ),
      body: _body(),
    );
  }

  Widget _itemBuilder(int index, int element) {
    final showAddButton = index > 0;
    return Stack(
      clipBehavior: Clip.none,
      children: [
        Container(
          child: ListTile(
            title: Text('Todo List Item $element'),
            subtitle: Text('Add a new item after'),
          ),
          decoration: BoxDecoration(
            color: Colors.deepOrange,
            border: Border.all(color: Colors.yellow),
          ),
        ),
        if (showAddButton)
          Positioned(
            top: -24,
            right: 8,
            child: Container(
              clipBehavior: Clip.antiAlias,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: Colors.green,
              ),
              child: DeferPointer(
                child: IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {
                    print('add before $element');
                  },
                ),
              ),
            ),
          ),
      ],
    );
  }

  Widget _body() {
    // return Column(
    //   children: items.mapIndexed(_itemBuilder).toList(),
    // );
    return DeferredPointerHandler(
      child: ListView.builder(
        itemCount: items.length,
        hitTestBehavior: HitTestBehavior.translucent,
        itemBuilder: (context, index) {
          final element = items[index];
          return _itemBuilder(index, element);
        },
      ),
    );
  }
}

Также обратите внимание, что для этого больше не требуется обходной путь StackTappableOutside.

Другие вопросы по теме