Различное поведение виджета Flutter Stack() в разных местах

Я работаю над плавающей кнопкой действия с некоторыми особенностями: когда вы нажимаете на FloatingActionButton, некоторые виджеты InkWell становятся видимыми из Stack, где вы можете нажать на несколько вариантов. Когда я вставил его в свое приложение, я столкнулся с чем-то странным:

Если я добавлю уникальный виджет MyFAB в качестве опции home: в MaterialApp, анимация будет работать отлично, и вы сможете без проблем нажимать на маленькие InkWell виджеты:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:  MyFAB(),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Но если я добавлю MyFAB в Scaffold как floatingActionButton:, маленькие значки станут бесполезными, вы больше не сможете на них нажимать.

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: MyFAB(),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Я застрял на этом этапе. Что вызывает такое поведение? Как я могу без проблем использовать MyFAB внутри Scaffold?

Вы можете опробовать мой код в https://dartpad.dev/

Я перекодировал весь виджет MyFAB, чтобы не использовать Stack, удалил опцию floatingActionButtonLocation из Scaffold, ничего не помогло.

@Иво, это определенно выглядит многообещающе, я проверю. Я никогда раньше не проводил тест на попадание в VS Code. Не могли бы вы предоставить мне несколько статей об этом?

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

Ответы 2

Проблема, с которой вы столкнулись, когда виджеты InkWell не доступны для щелчка, когда MyFAB() используется в качестве плавающей кнопки ActionButton в Scaffold, вероятно, связана с позиционированием и тестированием попадания виджетов в стек. При использовании floatActionButton в Scaffold стек плавающей кнопки действия может не охватывать весь экран, или виджеты в стеке могут быть неправильно расположены в области касания.

Чтобы решить эту проблему, вы можете убедиться, что стек, содержащий виджеты InkWell, занимает весь экран и расположен правильно. Один из подходов — использовать виджет «Позиционированный», чтобы правильно разместить кнопку плавающего действия и параметры в стеке.

Эй, можешь просто проверить это !!!

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Stack(
          children: [
            // Your main content goes here
          ],
        ),
        floatingActionButton: MyFAB(),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      clipBehavior: Clip.none,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        Positioned(
          bottom: 16.0, // Adjust the position as needed
          child: FloatingActionButton(
            heroTag: "MyFAB",
            onPressed: _toggle,
            shape: const CircleBorder(),
            child: AnimatedIcon(
              icon: AnimatedIcons.menu_close,
              progress: _animation,
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Стек внутри Scaffold гарантирует, что виджеты InkWell и FloatingActionButton расположены правильно и охватывают весь экран, делая их кликабельными. Свойство clipBehavior: Clip.none стека позволяет при необходимости рисовать виджеты InkWell за пределами стека. Виджет «Позиционирование» обеспечивает правильное размещение плавающей кнопки действия в нижней части экрана. Отрегулируйте нижнее свойство виджета «Позиционировано» по мере необходимости, чтобы получить желаемое положение.

Спасибо за Ваш ответ. К сожалению, ваш код не работает с dartpad. Я проверю это с помощью VS Code, как только закончу сегодняшнюю работу. :)

avemmortis 18.06.2024 10:23
Ответ принят как подходящий

Это потому, что тапируемая область в floatingActionButton привязана к дочернему виджету, а не к дополнительной кнопке 4, это иллюстрация, если дочерний виджет floatingActionButton обернут контейнером для увеличения области макета, отмеченной желтым цветом:

MaterialApp(
  home: Scaffold(
    floatingActionButton: Container(
      width: 120,
      height: 120,
      color: Colors.yellow,
      child: MyFAB(),
    ),
    floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
  ),
);

Результат:

Мой предлагаемый шаг ответа приведен ниже:

  1. Оберните FAB с помощью Container или SizedBox, чтобы изменить размер области касания.
  2. Измените свойство выравнивания стека в виджете MyFAB, чтобы выровнять кнопку по низу внутри контейнера.

Ниже приведен окончательный код, изменения отмечены комментариями:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: Container( // <- wrap to resize area
          width: 175,
          height: 115,
          color: Colors.yellow, // <- remove the color later
          child: MyFAB(),
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.bottomCenter, // <- align to bottom
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

И вот результат, цвет можно убрать:

Надеюсь, это решит вашу проблему 😉

Вы, сэр, не что иное, как гений! Огромное спасибо, теперь все работает отлично. 😊 Это какое-то требование самого виджета Stack()?

avemmortis 18.06.2024 17:37

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