You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现IconButton与ElevatedButton悬浮固定效果的方法

Flutter 悬浮按钮与滚动内容实现方案

需求说明

  • 左上角关闭IconButton保持悬浮,内容滚动时不移动,且不超出SafeArea范围
  • 底部ElevatedButton固定悬浮,不受内容滚动影响
  • 仅中间内容区域可滚动超出视图

原代码

import 'package:flutter/material.dart';
import 'package:icon_decoration/icon_decoration.dart';
import 'package:training/models/images_instance.dart';

class CustomDialog extends StatefulWidget {
  final int selectedIndex;

  const CustomDialog({super.key, required this.selectedIndex});

  @override
  State<CustomDialog> createState() => _CustomDialogState();
}

class _CustomDialogState extends State<CustomDialog> {

  ValueNotifier<double> animationValue = ValueNotifier<double>(0.0);

  double calculateItemPrice(int itemCount, double originalPrice) {
    return itemCount * originalPrice;
  }

  void addItem(int index) {
    setState(() {
      imagesInstances[index].itemCount++;
      animationValue.value = 0.5;
    });
  }

  void removeItem(int index) {
    setState(() {
      if (imagesInstances[index].itemCount > 1) {
        imagesInstances[index].itemCount--;
        animationValue.value = -0.5;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;

    return Column(
      children: [
        Stack(
          children: [
            ClipRRect(
              borderRadius: const BorderRadius.only(
                topLeft: Radius.circular(20),
                topRight: Radius.circular(20),
              ),
              child: Image.asset(
                imagesInstances[widget.selectedIndex].imageString!,
                width: size.width, // Stretch to the entire width
                height: size.height * 0.4,
                fit: BoxFit.cover,
              ),
            ),
            Positioned(
              top: 0,
              left: 0,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: CircleAvatar(
                  backgroundColor: Colors.white,
                  child: IconButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    icon: const Icon(
                      Icons.close,
                      color: Colors.black,
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
        Container(
          color: Colors.white,
          child: Padding(
            padding: const EdgeInsets.only(bottom: 10, right: 10, left: 10),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  imagesInstances[widget.selectedIndex].imageTitle!,
                  style: const TextStyle(
                    fontSize: 35,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(
                  height: 10,
                ),
                Text(
                  imagesInstances[widget.selectedIndex].imageDescription!,
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.grey.withOpacity(0.8),
                  ),
                ),
                const SizedBox(
                  height: 10,
                ),
                SizedBox(
                  width: double.infinity,
                  height: 80,
                  child: Stack(
                    children: [
                      Text(
                        'EPG ${imagesInstances[widget.selectedIndex].itemPrice.toString()}',
                        style: const TextStyle(
                          fontSize: 25,
                          fontWeight: FontWeight.bold,
                          color: Colors.black,
                        ),
                      ),
                      Positioned(
                        right: 0,
                        top: 10,
                        child: FittedBox(
                          child: Container(
                            decoration: BoxDecoration(
                              color: Colors.white,
                              borderRadius: BorderRadius.circular(11),
                              boxShadow: const [
                                BoxShadow(
                                  offset: Offset(1, 3),
                                  blurRadius: 4,
                                  color: Colors.grey,
                                ),
                              ],
                            ),
                            child: Row(
                              mainAxisSize: MainAxisSize.min,
                              children: [
                                IconButton(
                                  onPressed: () {
                                    removeItem(widget.selectedIndex);
                                  },
                                  icon: Icon(
                                    Icons.remove,
                                    color: imagesInstances[widget.selectedIndex]
                                        .itemCount > 1
                                        ? const Color(0xffE66219)
                                        : const Color(0xffFECDB4),
                                  ),
                                ),
                                AnimatedSwitcher(
                                  transitionBuilder: (child, animation) {
                                    final   offset = Offset(0, animationValue.value);
                                    return   SlideTransition(
                                      position: Tween<Offset>(
                                        begin: offset,
                                        end: Offset.zero,
                                      ).animate(animation),
                                      child: child,
                                    );

                                  },


                                  duration: const Duration(milliseconds: 50),
                                  child: Text(
                                    '${imagesInstances[widget.selectedIndex].itemCount}',
                                    key: Key(
                                        '${imagesInstances[widget.selectedIndex].itemCount}'),
                                    style: const TextStyle(fontSize: 25),
                                  ),
                                ),
                                IconButton(
                                  color: const Color(0xffE66219),
                                  // Default color
                                  onPressed: () {
                                    addItem(widget.selectedIndex);
                                  },
                                  icon: const Icon(Icons.add),
                                ),
                              ],
                            ),
                          ),
                        ),
                      )
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        const SizedBox(
          height: 10,
        ),
        Container(
          color: Colors.white,
          child: Column(
            children: [
              Padding(
                padding: const EdgeInsets.only(
                    left: 30, right: 15, top: 10, bottom: 10),
                child: Row(
                  children: [
                    Stack(
                      children: [
                        Transform.translate(
                          offset: const Offset(12, 0),
                          child: Transform(
                            transform: Matrix4.diagonal3Values(-1, 1, 1),
                            child: const DecoratedIcon(
                              icon: Icon(
                                Icons.chat_bubble,
                                color: Colors.white,
                              ),
                              decoration: IconDecoration(
                                border:
                                IconBorder(width: 3, color: Colors.black),
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                    const Text(
                      'Any special requests?',
                      style: TextStyle(
                        fontSize: 15,
                        fontWeight: FontWeight.bold,
                        color: Colors.black,
                      ),
                    ),
                    const Spacer(),
                    const Text(
                      'Add note',
                      style: TextStyle(
                        fontSize: 15,
                        fontWeight: FontWeight.bold,
                        color: Colors.orange,
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(
                height: 20,
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
                child: ElevatedButton(
                  onPressed: () {},
                  style: ElevatedButton.styleFrom(
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(5),
                    ),
                    padding: const EdgeInsets.symmetric(
                        vertical: 15, horizontal: 15),
                    backgroundColor: Colors.orange[900],
                  ),
                  child: Row(
                    children: [
                      const Text(
                        'Add to basket',
                        style: TextStyle(
                          fontSize: 20,
                        ),
                      ),
                      const Spacer(),
                      Text(
                        'EPG ${calculateItemPrice(imagesInstances[widget.selectedIndex].itemCount, imagesInstances[widget.selectedIndex].itemPrice!)}',
                        style: const TextStyle(
                          fontSize: 20,
                          color: Colors.white,
                        ),
                      ),


                    ],
                  ),
                ),
              )
            ],
          ),
        ),
      ],
    );
  }
}

void showCustomDialog(BuildContext context, int selectedIndex) {
  showModalBottomSheet(
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.only(
        topRight: Radius.circular(20),
        topLeft: Radius.circular(20),
      ),
    ),
    isScrollControlled: true, // Use isScrollControlled
    context: context,
    builder: (BuildContext context) {
      return SingleChildScrollView(
        child: CustomDialog(selectedIndex: selectedIndex),
      );
    },
  );
}

修改方案

核心思路是用Stack作为最外层容器,将悬浮组件(关闭按钮、底部按钮)放在顶层,中间可滚动内容放在底层,同时通过SafeArea确保组件不超出安全区域。

具体修改点

  1. 最外层改用Stack:替代原来的Column,让悬浮组件能覆盖在滚动内容之上
  2. 关闭按钮添加SafeArea:避免在刘海屏等设备上超出顶部安全区域
  3. 中间内容用SingleChildScrollView包裹:设置底部padding留出固定按钮的空间,确保内容滚动时不会被按钮遮挡
  4. 底部按钮固定定位:用Positioned将按钮固定在底部,同时适配SafeArea

修改后的完整代码

import 'package:flutter/material.dart';
import 'package:icon_decoration/icon_decoration.dart';
import 'package:training/models/images_instance.dart';

class CustomDialog extends StatefulWidget {
  final int selectedIndex;

  const CustomDialog({super.key, required this.selectedIndex});

  @override
  State<CustomDialog> createState() => _CustomDialogState();
}

class _CustomDialogState extends State<CustomDialog> {
  ValueNotifier<double> animationValue = ValueNotifier<double>(0.0);

  double calculateItemPrice(int itemCount, double originalPrice) {
    return itemCount * originalPrice;
  }

  void addItem(int index) {
    setState(() {
      imagesInstances[index].itemCount++;
      animationValue.value = 0.5;
    });
  }

  void removeItem(int index) {
    setState(() {
      if (imagesInstances[index].itemCount > 1) {
        imagesInstances[index].itemCount--;
        animationValue.value = -0.5;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    final bottomButtonHeight = 100.0; // 底部按钮区域高度,用于设置滚动内容的padding

    return Stack(
      children: [
        // 可滚动的内容区域
        SingleChildScrollView(
          padding: EdgeInsets.only(bottom: bottomButtonHeight + 20), // 留出底部按钮的空间
          child: Column(
            children: [
              // 顶部图片区域
              ClipRRect(
                borderRadius: const BorderRadius.only(
                  topLeft: Radius.circular(20),
                  topRight: Radius.circular(20),
                ),
                child: Image.asset(
                  imagesInstances[widget.selectedIndex].imageString!,
                  width: size.width,
                  height: size.height * 0.4,
                  fit: BoxFit.cover,
                ),
              ),
              // 商品信息区域
              Container(
                color: Colors.white,
                child: Padding(
                  padding: const EdgeInsets.only(bottom: 10, right: 10, left: 10),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        imagesInstances[widget.selectedIndex].imageTitle!,
                        style: const TextStyle(
                          fontSize: 35,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 10),
                      Text(
                        imagesInstances[widget.selectedIndex].imageDescription!,
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                          color: Colors.grey.withOpacity(0.8),
                        ),
                      ),
                      const SizedBox(height: 10),
                      SizedBox(
                        width: double.infinity,
                        height: 80,
                        child: Stack(
                          children: [
                            Text(
                              'EPG ${imagesInstances[widget.selectedIndex].itemPrice.toString()}',
                              style: const TextStyle(
                                fontSize: 25,
                                fontWeight: FontWeight.bold,
                                color: Colors.black,
                              ),
                            ),
                            Positioned(
                              right: 0,
                              top: 10,
                              child: FittedBox(
                                child: Container(
                                  decoration: BoxDecoration(
                                    color: Colors.white,
                                    borderRadius: BorderRadius.circular(11),
                                    boxShadow: const [
                                      BoxShadow(
                                        offset: Offset(1, 3),
                                        blurRadius: 4,
                                        color: Colors.grey,
                                      ),
                                    ],
                                  ),
                                  child: Row(
                                    mainAxisSize: MainAxisSize.min,
                                    children: [
                                      IconButton(
                                        onPressed: () => removeItem(widget.selectedIndex),
                                        icon: Icon(
                                          Icons.remove,
                                          color: imagesInstances[widget.selectedIndex].itemCount > 1
                                              ? const Color(0xffE66219)
                                              : const Color(0xffFECDB4),
                                        ),
                                      ),
                                      AnimatedSwitcher(
                                        transitionBuilder: (child, animation) {
                                          final offset = Offset(0, animationValue.value);
                                          return SlideTransition(
                                            position: Tween<Offset>(
                                              begin: offset,
                                              end: Offset.zero,
                                            ).animate(animation),
                                            child: child,
                                          );
                                        },
                                        duration: const Duration(milliseconds: 50),
                                        child: Text(
                                          '${imagesInstances[widget.selectedIndex].itemCount}',
                                          key: Key('${imagesInstances[widget.selectedIndex].itemCount}'),
                                          style: const TextStyle(fontSize: 25),
                                        ),
                                      ),
                                      IconButton(
                                        color: const Color(0xffE66219),
                                        onPressed: () => addItem(widget.selectedIndex),
                                        icon: const Icon(Icons.add),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                            )
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 10),
              // 备注区域
              Container(
                color: Colors.white,
                child: Padding(
                  padding: const EdgeInsets.only(left: 30, right: 15, top: 10, bottom: 10),
                  child: Row(
                    children: [
                      Stack(
                        children: [
                          Transform.translate(
                            offset: const Offset(12, 0),
                            child: Transform(
                              transform: Matrix4.diagonal3Values(-1, 1, 1),
                              child: const DecoratedIcon(
                                icon: Icon(
                                  Icons.chat_bubble,
                                  color: Colors.white,
                                ),
                                decoration: IconDecoration(
                                  border: IconBorder(width: 3, color: Colors.black),
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                      const Text(
                        'Any special requests?',
                        style: TextStyle(
                          fontSize: 15,
                          fontWeight: FontWeight.bold,
                          color: Colors.black,
                        ),
                      ),
                      const Spacer(),
                      const Text(
                        'Add note',
                        style: TextStyle(
                          fontSize: 15,
                          fontWeight: FontWeight.bold,
                          color: Colors.orange,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
        // 悬浮关闭按钮(带SafeArea)
        Positioned(
          top: 0,
          left: 0,
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: CircleAvatar(
                backgroundColor: Colors.white,
                child: IconButton(
                  onPressed: () => Navigator.pop(context),
                  icon: const Icon(Icons.close, color: Colors.black),
                ),
              ),
            ),
          ),
        ),
        // 悬浮底部按钮
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: SafeArea(
            child: Container(
              color: Colors.white,
              padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
              child: ElevatedButton(
                onPressed: () {},
                style: ElevatedButton.styleFrom(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(5),
                  ),
                  padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 15),
                  backgroundColor: Colors.orange[900],
                ),
                child: Row(
                  children: [
                    const Text(
                      'Add to basket',
                      style: TextStyle(fontSize: 20),
                    ),
                    const Spacer(),
                    Text(
                      'EPG ${calculateItemPrice(imagesInstances[widget.selectedIndex].itemCount, imagesInstances[widget.selectedIndex].itemPrice!)}',
                      style: const TextStyle(fontSize: 20, color: Colors.white),
                    ),
                  ],
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 04:12:49