在 Flutter 中点击 Stack 外的 Widget

问题描述 投票:0回答:1

目前存在一个已知的 Flutter 限制,它会使小部件溢出

Stack
并且无法接收手势事件。

但是,有一个 hack 似乎可以通过扩展

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(),
  ));
}

enter image description here

使用上面的代码,当您从任何地方点击绿色按钮时,它会打印:

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

flutter dart user-interface
1个回答
0
投票

似乎很奇怪,似乎没有简单的开箱即用的方法来做到这一点。但我发现了一个不错的 pub 包,它似乎适合我: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);
        },
      ),
    );
  }
}

© www.soinside.com 2019 - 2024. All rights reserved.