Flutter for OpenHarmony 实战之基础组件:第三篇 Stack 层叠布局详解

简介: 本文详解 Flutter for OpenHarmony 中 Stack 层叠布局:涵盖 Z 轴堆叠原理、Alignment 对齐与 Positioned 精准定位,实战 Badge 角标、新闻 Banner 及 IndexedStack 页面状态保持,助你高效构建跨端复杂 UI。(239字)

Flutter for OpenHarmony 实战之基础组件:第三篇 Stack 层叠布局详解

前言

如果你习惯了 CSS 中的 absolute(绝对定位)或者 Android 中的 FrameLayout,那么 Flutter 中的 Stack 对你来说将非常亲切。

RowColumn 解决了线性排列的问题,而 Stack 则解决了重叠展示的需求。它是构建复杂 UI(如视频播放器界面、带角标的图标、全屏加载动画)不可或缺的工具。

本文你将学到

  • Stack 的对齐原理与层级顺序
  • Positioned 与 Align 的精准定位
  • 构建带角标(Badge)的通用组件
  • 实战:打造精美的新闻 Banner 组件
  • 性能优化:IndexedStack 在鸿蒙开发中的应用

一、Stack 基础概念

1.1 堆叠原理 (Z-Order)

Stack 像一叠扑克牌,子组件按照添加顺序从下往上堆叠:

  1. children 列表中的第一个组件在最底层 (Bottom)。
  2. children 列表中的最后一个组件在最顶层 (Top)。

my_first_app/lib/widgets/basic_stack.dart

import 'package:flutter/material.dart';

class BasicStack extends StatelessWidget {
   
  const BasicStack({
   super.key});

  
  Widget build(BuildContext context) {
   
    return Stack(
      children: [
        // 底层:绿色背景
        Container(width: 300, height: 300, color: Colors.green),

        // 中层:黄色方块
        Container(width: 200, height: 200, color: Colors.yellow),

        // 顶层:红色圆圈
        Container(width: 100, height: 100, color: Colors.red),
      ],
    );
  }
}

my_first_app/lib/main.dart

import 'package:flutter/material.dart';
import 'package:my_first_app/widgets/basic_stack.dart';

/// 应用入口函数
void main() {
  // 运行 Flutter 应用
  runApp(const MyApp());
}

/// 根 Widget - 应用程序的顶层组件
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter for OpenHarmony', // 应用标题
      debugShowCheckedModeBanner: false, // 隐藏调试标签
      theme: ThemeData(
        // 主题配置
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true, // 使用 Material 3 设计
      ),
      home: const HomePage(), // 首页
    );
  }
}

/// 首页 Widget
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 应用栏
      appBar: AppBar(
        title: const Text('我的第一个鸿蒙应用 By 王码码'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      // 页面主体
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BasicStack(),
          ],
        ),
      ),
      // 悬浮按钮
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 显示提示
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Flutter + OpenHarmony = ❤️')),
          );
        },
        child: const Icon(Icons.favorite),
      ),
    );
  }
}

在这里插入图片描述

1.2 对齐方式 (Alignment)

当子组件没有被 Positioned 包裹时,它们是非定位(non-positioned)组件,由 Stack.alignment 属性统一控制位置。

my_first_app/lib/widgets/basic_alignment.dart

import 'package:flutter/material.dart';

class BasicAlignment extends StatelessWidget {
   
  const BasicAlignment({
   super.key});

  
  Widget build(BuildContext context) {
   
    return Stack(
      alignment: Alignment.center, // 所有非定位子组件居中
      children: [
        Container(width: 200, height: 200, color: Colors.blue),
        const Text('Center Text', style: TextStyle(color: Colors.white)),
      ],
    );
  }
}

Alignment 常用坐标系

  • (0, 0): 中心点
  • (-1, -1): 左上角
  • (1, 1): 右下角
  • (0, 1): 底部中心

在这里插入图片描述


二、精准定位:Positioned 组件

Positioned 组件只能作为 Stack 的直接子节点使用,用于控制子组件的精确位置和尺寸。

2.1 核心属性

  • left, top, right, bottom: 距离 Stack 边缘的距离。
  • width, height: 强制指定子组件尺寸。

my_first_app/lib/widgets/basic_position.dart

import 'package:flutter/material.dart';

class BasicPosition extends StatelessWidget {
   
  const BasicPosition({
   super.key});

  
  Widget build(BuildContext context) {
   
    return Stack(
      children: [
        // 底图
        Container(color: Colors.grey[200], height: 200),

        // 左上角距离 10px
        const Positioned(
          left: 10,
          top: 10,
          child: const Icon(Icons.star, color: Colors.orange),
        ),

        // 底部横幅 (left=0, right=0 相当于只有 width=parentWidth)
        Positioned(
          left: 0,
          right: 0,
          bottom: 0,
          height: 40,
          child: Container(
            color: Colors.black54,
            alignment: Alignment.center,
            child: const Text('底部悬浮条', style: TextStyle(color: Colors.white)),
          ),
        ),
      ],
    );
  }
}

在这里插入图片描述

2.2 常见误区

错误用法:在 Stack 外部使用 Positioned

Column(
  children: [
    Positioned(...) // 报错:Positioned 必须是 Stack 的子组件
  ],
)

技巧:Positioned.fill
如果想让子组件填满整个 Stack,可以使用简写:

Positioned.fill(
  child: Image.asset('bg.png', fit: BoxFit.cover),
)

三、实战案例 1:消息角标 (Badge) 组件

这是 APP 中最常见的设计:头像右上角有个红色未读数字。

my_first_app/lib/widgets/avatar_badge.dart

import 'package:flutter/material.dart';

class AvatarWithBadge extends StatelessWidget {
   
  final String imageUrl;
  final int count;

  const AvatarWithBadge({
   
    super.key,
    required this.imageUrl,
    required this.count,
  });

  
  Widget build(BuildContext context) {
   
    return Stack(
      // 允许子组件略微超出 Stack 范围 (clipBehavior 默认是 hardEdge,需要改为 none)
      clipBehavior: Clip.none,
      children: [
        // 1. 头像
        Container(
          width: 60,
          height: 60,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            image: DecorationImage(
              // 根据路径自动选择图片来源:本地资源 or 网络
              image: imageUrl.startsWith('http')
                  ? NetworkImage(imageUrl)
                  : AssetImage(imageUrl) as ImageProvider,
              fit: BoxFit.cover,
            ),
            border: Border.all(color: Colors.white, width: 2),
            boxShadow: const [
              BoxShadow(
                color: Colors.black12,
                blurRadius: 4,
                offset: Offset(0, 2),
              ),
            ],
          ),
        ),

        // 2. 红色角标 (仅当数量 > 0 时显示)
        if (count > 0)
          Positioned(
            right: -4, // 向右偏移,制造破局跟随效果
            top: -4, // 向上偏移
            child: Container(
              padding: const EdgeInsets.all(4),
              constraints: const BoxConstraints(
                minWidth: 20,
                minHeight: 20,
              ),
              decoration: BoxDecoration(
                color: Colors.red,
                shape: BoxShape.circle,
                border: Border.all(color: Colors.white, width: 1.5),
              ),
              child: Center(
                child: Text(
                  count > 99 ? '99+' : count.toString(),
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 10,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),
          ),
      ],
    );
  }
}

my_first_app/lib/main.dart

import 'package:flutter/material.dart';
import 'package:my_first_app/widgets/basic_position.dart';
import 'package:my_first_app/widgets/basic_stack.dart';
import 'package:my_first_app/widgets/basic_alignment.dart';
import 'package:my_first_app/widgets/avatar_badge.dart';

/// 应用入口函数
void main() {
   
  // 运行 Flutter 应用
  runApp(const MyApp());
}

/// 根 Widget - 应用程序的顶层组件
class MyApp extends StatelessWidget {
   
  const MyApp({
   super.key});

  
  Widget build(BuildContext context) {
   
    return MaterialApp(
      title: 'Flutter for OpenHarmony', // 应用标题
      debugShowCheckedModeBanner: false, // 隐藏调试标签
      theme: ThemeData(
        // 主题配置
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true, // 使用 Material 3 设计
      ),
      home: const HomePage(), // 首页
    );
  }
}

/// 首页 Widget
class HomePage extends StatelessWidget {
   
  const HomePage({
   super.key});

  
  Widget build(BuildContext context) {
   
    return Scaffold(
      // 应用栏
      appBar: AppBar(
        title: const Text('我的第一个鸿蒙应用 By 王码码'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      // 页面主体
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // BasicStack(),
            // BasicAlignment(),
            // BasicPosition(),
            AvatarWithBadge(
              imageUrl:
                  'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120&h=120&fit=crop', // Unsplash 头像
              count: 5,
            ),
          ],
        ),
      ),
      // 悬浮按钮
      floatingActionButton: FloatingActionButton(
        onPressed: () {
   
          // 显示提示
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Flutter + OpenHarmony = ❤️')),
          );
        },
        child: const Icon(Icons.favorite),
      ),
    );
  }
}

在这里插入图片描述


四、实战案例 2:新闻 Banner 组件

我们需要实现一个典型的 Banner 效果:图片 + 渐变蒙层 + 左下角文字 + 右下角指示器。

my_first_app/lib/widgets/news_banner.dart

import 'package:flutter/material.dart';

class NewsBanner extends StatelessWidget {
   
  final String title;
  final String imageUrl;
  final String tag;

  const NewsBanner({
   
    super.key,
    required this.title,
    required this.imageUrl,
    required this.tag,
  });

  
  Widget build(BuildContext context) {
   
    return Container(
      height: 200,
      margin: const EdgeInsets.all(16),
      // ClipRRect 用于裁剪 Stack 的圆角
      child: ClipRRect(
        borderRadius: BorderRadius.circular(12),
        child: Stack(
          children: [
            // 1. 背景图 (填满)
            Positioned.fill(
              child: Image.network(
                imageUrl,
                fit: BoxFit.cover,
              ),
            ),

            // 2. 渐变蒙层 (提升文字可读性)
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              height: 100, // 仅底部有渐变
              child: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [
                      Colors.transparent,
                      Colors.black.withOpacity(0.8),
                    ],
                  ),
                ),
              ),
            ),

            // 3. 标签 (左上角)
            Positioned(
              left: 12,
              top: 12,
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Text(
                  tag,
                  style: const TextStyle(color: Colors.white, fontSize: 10),
                ),
              ),
            ),

            // 4. 标题 (左下角)
            Positioned(
              left: 16,
              bottom: 16,
              right: 60, // 留出空间给指示器等
              child: Text(
                title,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

my_first_app/lib/main.dart

import 'package:flutter/material.dart';
import 'package:my_first_app/widgets/basic_position.dart';
import 'package:my_first_app/widgets/basic_stack.dart';
import 'package:my_first_app/widgets/basic_alignment.dart';
import 'package:my_first_app/widgets/avatar_badge.dart';
import 'package:my_first_app/widgets/news_banner.dart';

/// 应用入口函数
void main() {
   
  // 运行 Flutter 应用
  runApp(const MyApp());
}

/// 根 Widget - 应用程序的顶层组件
class MyApp extends StatelessWidget {
   
  const MyApp({
   super.key});

  
  Widget build(BuildContext context) {
   
    return MaterialApp(
      title: 'Flutter for OpenHarmony', // 应用标题
      debugShowCheckedModeBanner: false, // 隐藏调试标签
      theme: ThemeData(
        // 主题配置
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true, // 使用 Material 3 设计
      ),
      home: const HomePage(), // 首页
    );
  }
}

/// 首页 Widget
class HomePage extends StatelessWidget {
   
  const HomePage({
   super.key});

  
  Widget build(BuildContext context) {
   
    return Scaffold(
      // 应用栏
      appBar: AppBar(
        title: const Text('我的第一个鸿蒙应用 By 王码码'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      // 页面主体
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // BasicStack(),
            // BasicAlignment(),
            // BasicPosition(),
            // AvatarWithBadge(
            //   imageUrl:
            //       'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120&h=120&fit=crop', // Unsplash 头像
            //   count: 5,
            // ),
            NewsBanner(
              title: 'HarmonyOS NEXT 5.0 正式发布:纯血鸿蒙时代来临',
              imageUrl:
                  'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=600&h=400&fit=crop', // 科技发布会配图
              tag: '鸿蒙',
            ),
          ],
        ),
      ),
      // 悬浮按钮
      floatingActionButton: FloatingActionButton(
        onPressed: () {
   
          // 显示提示
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Flutter + OpenHarmony = ❤️')),
          );
        },
        child: const Icon(Icons.favorite),
      ),
    );
  }
}

在这里插入图片描述

五、鸿蒙开发技巧:IndexedStack

在 OpenHarmony 应用开发中,我们经常需要实现底部导航栏(BottomNavigationBar)的页面切换。

5.1 传统方式的痛点

如果直接使用 body: pages[currentIndex],每次切换页面时,原来的页面会被销毁,新页面会被重建。如果页面包含复杂的网络请求或地图组件,重建成本非常高,且无法保持滚动位置。

5.2 使用 IndexedStack 优化

IndexedStack 是一个特殊的 Stack,它一次性加载所有子组件,但只显示 index 对应的那个。其他组件虽然不可见,但状态依然保持(State Keep Alive)。

在这里插入图片描述

my_first_app/lib/main_page.dart

import 'package:flutter/material.dart';
import 'package:my_first_app/main.dart';
import 'package:my_first_app/category_page.dart';
import 'package:my_first_app/profile_page.dart';

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

  
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
   
  int _currentIndex = 0;

  // 页面列表
  final List<Widget> _pages = const [
    HomePage(),
    CategoryPage(),
    ProfilePage(),
  ];

  
  Widget build(BuildContext context) {
   
    return Scaffold(
      appBar: AppBar(title: const Text('IndexedStack 性能优化')),

      // ✅ 优化:使用 IndexedStack 保持页面状态
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),

      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
   
          setState(() {
   
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.category), label: '分类'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

my_first_app/lib/category_page.dart

import 'package:flutter/material.dart';

class CategoryPage extends StatelessWidget {
   
  const CategoryPage({
   super.key});

  
  Widget build(BuildContext context) {
   
    return const Center(
      child: Text('分类页面'),
    );
  }
}

my_first_app/lib/profile_page.dart

import 'package:flutter/material.dart';

class ProfilePage extends StatelessWidget {
   
  const ProfilePage({
   super.key});

  
  Widget build(BuildContext context) {
   
    return const Center(
      child: Text('我的页面'),
    );
  }
}

my_first_app/lib/main.dart

// ...
import 'package:my_first_app/main_page.dart';

// ...省略
/// 根 Widget - 应用程序的顶层组件
class MyApp extends StatelessWidget {
   
  const MyApp({
   super.key});

  
  Widget build(BuildContext context) {
   
    return MaterialApp(
      title: 'Flutter for OpenHarmony', // 应用标题
      debugShowCheckedModeBanner: false, // 隐藏调试标签
      theme: ThemeData(
        // 主题配置
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true, // 使用 Material 3 设计
      ),
      home: const MainPage(), // 首页
    );
  }
}
// ...省略

⚠️ 性能注意IndexedStack 适合页面数量较少(3-5 个)且需要保持状态的场景。如果页面非常多,建议配合 PageView 或自定义缓存策略。


六、层叠上下文陷阱 (Z-Index 不存在?)

Flutter 中没有 Web 开发中的 z-index 属性。如果你想调整层级,只能通过调整 children 列表的顺序

如何让点击事件穿透?
有时候上层的透明蒙层会挡住下层按钮的点击事件。

  • 使用 IgnorePointer: 忽略自己和子组件的点击事件。
  • 使用 AbsorbPointer: 吸收点击事件,不让其传递给下层。
Stack(
  children: [
    ElevatedButton(onPressed: () {
   }, child: Text('点我')),

    // 这个透明层会挡住按钮点击
    // 解决方法:包裹 IgnorePointer
    IgnorePointer(
      child: Container(color: Colors.transparent),
    ),
  ],
)

七、总结

Stack 赋予了我们在 Z 轴上的布局能力,配合 Positioned 可以实现像素级的精确控制。

核心知识点

  1. 层叠顺序:代码在后的在上面。
  2. 定位控制:使用 Positioned + top/bottom/left/right
  3. 溢出处理:设置 clipBehavior: Clip.none 允许子组件画出界。
  4. 性能优化:状态保持场景优先选 IndexedStack

下一篇预告

我们已经掌握了基本的布局和定位。接下来的文章我们将进入最基础但也最复杂的组件——文本
《Flutter for OpenHarmony 实战之基础组件:第四篇 Text 文本组件全解》
我们将讨论富文本(RichText)、文本溢出处理、自定义字体以及鸿蒙系统下的字体适配。

相关文章
|
Dart Android开发 iOS开发
比较 Flutter 日期选择器库【Flutter 专题 6】
日期选择器是对 UI 的有用补充,它使您的应用程序用户可以轻松地从日历中选择日期。无论您是在注册表单中添加出生日期字段,还是为用户提供预约时间段,您都可以使用日期选择器库来简化流程。 在本教程中,我们将探索 Flutter 的三个流行日期选择器库 Flutter、Flutter Datetime Picker、Flutter Date Range Picker 和date_time_picker。我们将检查每个库的功能并将每个库安装在一个简单的移动应用程序中。
1614 0
比较 Flutter 日期选择器库【Flutter 专题 6】
|
13天前
|
设计模式 算法 物联网
【免费开源】STM32矩阵键盘驱动程序:从零搭建4x4键盘扫描与消抖完整实战项目分享
【免费开源】STM32 4×4矩阵键盘驱动,基于HAL库,支持行列/反转双扫描、软件消抖、短按/长按/连按识别及回调机制,兼容F1/F4系列,仅需8个IO,附完整原理、流程图与可移植代码。
|
14天前
|
数据采集 网络协议 程序员
如果你天天用 requests.get(),请务必读懂这篇文章
本文深度剖析Requests底层原理,揭秘HTTP请求全链路:从Session调度、PreparedRequest格式化,到HTTPAdapter适配、ConnectionPool连接复用,直至socket层I/O。厘清代理介入时机与报错根因,附高并发爬虫最佳实践。
161 3
|
14天前
|
存储 缓存 固态存储
程序员必备的十大技能(进阶版)之底层计算机原理(四)
教程来源 oplhc.cn 本文深入解析存储I/O与性能极限:涵盖HDD/SSD物理特性、Linux I/O栈及零拷贝优化;剖析CPU功耗墙、内存墙与Amdahl定律;并结合缓存友好设计、伪共享规避、分支预测优化及SIMD向量化等底层编程实践,助力高性能系统开发。
|
1月前
|
存储 NoSQL 关系型数据库
7-事务控制篇-1
关系型数据库(如MySQL)基于表结构与SQL,强事务(ACID)、支持复杂查询;非关系型数据库(如Redis、MongoDB)采用键值、文档等灵活模型,高性能、易扩展,适用于高并发与非结构化数据,但弱于事务与关联查询。(239字)
126 6
|
11天前
|
安全 前端开发 搜索推荐
2026 世界杯移动端定向钓鱼攻击模式与防御技术研究
本文剖析2026年世界杯期间三类规模化移动端钓鱼攻击:票务仿冒、体育零售诈骗及赛事招聘AiTM中间人攻击。揭示其利用域名仿冒、CDN隐匿、MFA实时绕过等技术窃取隐私、金融与企业账号的黑产链条,并提出终端检测、行为分析与人员管控相结合的综合防御方案。(240字)
73 0
|
1月前
|
前端开发 数据库 数据安全/隐私保护
搭建互联网医院系统:医疗资质对接与合规建设解析
互联网医院开发难点不在界面,而在资质合规、多系统对接(HIS/EMR/医保/处方平台)与数据安全。需构建可审计的日志体系、智能接口中台及全流程加密机制,实现医疗协同而非简单线上问诊。
|
2月前
|
Android开发 C++ 容器
Flutter for OpenHarmony 实战之基础组件:第二篇 Row 与 Column 弹性布局详解
本文详解 Flutter for OpenHarmony 中核心布局组件 Row 与 Column:厘清主轴/交叉轴概念,对比 Expanded、Flexible 与 Spacer 的适用场景,提供溢出警告(RenderFlex overflowed)的实战解决方案,并结合复杂列表项与鸿蒙折叠屏动态横竖排布局,助你高效构建响应式 UI。(239字)
138 0
Flutter for OpenHarmony 实战之基础组件:第二篇 Row 与 Column 弹性布局详解
|
2月前
|
编解码 Dart 前端开发
Flutter for OpenHarmony 实战之基础组件:第一篇 Container 容器组件完全指南
本文是 Flutter for OpenHarmony 开发的首篇基础组件指南,深度解析 Container 容器组件:涵盖尺寸控制、margin/padding 区别、Alignment 对齐、BoxDecoration 装饰(渐变/圆角/阴影),并重点讲解 OpenHarmony 多分辨率适配、刘海屏安全区处理及性能优化技巧,助你打造高质量跨端 UI。(239字)
172 1
Flutter for OpenHarmony 实战之基础组件:第一篇 Container 容器组件完全指南