30天计划第6天-Flutter基础布局(2)

-
-
2025-10-30 12:23
  • 学习内容

    • 弹性布局:Expanded(占满剩余空间)、Flex

    • 层叠布局:Stack(组件叠放)、Positioned(固定位置)

  • 实践任务

    • Row+Expanded 做一个 “底部导航栏”:3 个按钮,每个占 1/3 宽度

    • Stack 做一个 “图片水印”:背景图 + 右上角文字水印

一、前置只是:两种布局的核心场景

1. 弹性布局(Expanded/Flex)

  • 核心是“空间分配”——当父组件有剩余空间时,按比例分配给子组件;或子组件超出空间时,按比例收缩。

  • 适用于“组件需要自适应父容器大小”的场景(如:按钮栏、多列文本布局)。

2. 层叠布局(Stack/Positioned)

  • 核心是“组件叠放”——将多个组件按先后顺序堆叠,后面的组件覆盖前面的。

  • 适用于“组件需要重叠显示”的场景(如:图片上的标签、悬浮按钮、弹窗)。

二、弹性布局:Expanded + Flex

1. 核心组件关系

  • Flex:弹性布局的基础容器,支持水平(Axis.horizontal)或垂直(Axis.vertical)方向,Row和Column本质是继承自Flex的简化版(Row=Flex(direction: Axis.horizontal), Column=Flex(direction: Axis.vertical))。

  • Expanded:必须作为Flex(或Row/Column)的直接子组件,租用是“强制子组件填充父组件的剩余空间”,通过flex属性控制空间占比。

2. Flex组件核心属性

属性名作用常用值
direction布局方向(主轴)Axis.horizontal(水平)、Axis.vertical(垂直)
children子组件列表(可包含Expanded或普通组件)任意Widget组合
mainAxisAlignment主轴对齐方式(同Row/Column)MainAxisAlignment.start、center等
crossAxisAlignment交叉轴对齐方式(同Row/Column)CrossAxisAlignment.center、stretch等
mainAxisSize主轴占用空间(同Row/Column)MainAxisSize.max(默认占满)、MainAxisSize.min(仅占子组件总大小)

3. Expanded组件核心属性

属性名作用常用值
flex剩余空间分配比例(默认值1)整数(如1、2、3)
child被拉伸的子组件任意Widget(如Container、Text)

4. 示例代码

示例1:基础弹性布局(Row + Expanded分配水平空间)

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Expanded 基础示例')),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Expanded(flex: 1, child: ElevatedButton(onPressed: () {}, child: const Text('按钮1'))),
              const SizedBox(width: 8),
              Expanded(flex: 2, child: ElevatedButton(onPressed: () {}, child: const Text('按钮2'))),
              SizedBox(width: 8),
              Expanded(flex: 1, child: ElevatedButton(onPressed: () {}, child: const Text('按钮3')))
            ],
          ),
        ),
      ),
    );
  }
}
  • 运行效果

示例2:Flex组件的灵活用法(动态切换方向)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Flex组件示例')),
        body: Padding(
          padding: EdgeInsets.all(16),
          child: Flex(
            direction: Axis.vertical,
            children: [
              Expanded(
                flex: 2,
                child: Container(color: Colors.yellow, child: Center(child: Text('占两份'))),
              ),
              const SizedBox(height: 8),
              Expanded(
                flex: 1,
                child: Container(color: Colors.blue, child: Center(child: Text('占1份'))),
              )
            ],
            ),
          ),
      ),
    );
  }
}
  • 运行效果

示例3:嵌套弹性布局(复杂空间分配)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('嵌套弹性布局')),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              const Text('嵌套弹性布局示例', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
              const SizedBox(height: 16),
              Expanded(
                child: Row(
                  children: [
                    Expanded(
                      flex: 3,
                      child: Container(color: Colors.lightBlue, child: const Center(child: const Text('左侧(3份)'))),
                    ),
                    const SizedBox(width: 8),
                    Expanded(
                      flex: 2,
                      child: Container(color: Colors.lightGreen, child: const Center(child: const Text('右侧(2份)'))),
                    )
                  ],
                ),
              ),
              const SizedBox(height: 16),
              ElevatedButton(onPressed: (){}, child: const Text('确认'))
            ],
          ),
        ),
      ),
    );
  }
}
  • 运行效果

5. 弹性布局常见问题及解决方案

问题1:Expanded导致子组件溢出

  • 现象:当Expanded的子组件有固定宽高且总大小超过父组件的约束时,会出现溢出警告
  • 原因:Expanded强制子组件填充空间,但是子组件的最大大小超过了父组件能提供的空间。
  • 解决方案:给子组件添加constraints限制最大大小,或用Wrap替代Row/Column
// 错误示例:子组件固定宽度 200,父组件宽度不足时溢出
Expanded(
  child: Container(width: 200, height: 80, color: Colors.red),
)

// 正确示例:限制子组件最大宽度
Expanded(
  child: Container(
    constraints: const BoxConstraints(maxWidth: 150), // 最大宽度 150
    height: 80,
    color: Colors.red,
  ),
)

问题2: Expanded不生效(子组件未拉伸)

  • 原因:Expanded必须是Flex(Row/Column/Flex)的直接子组件,若嵌套在其他组件(如Container)中则不生效
  • 错误示例
Row(
  children: [
    Container(
      // Expanded 嵌套在 Container 中,不生效
      child: Expanded(child: Text("不会拉伸")),
    ),
  ],
)
  • 正确写法
Row(
  children: [
    Expanded(
      // Expanded 是 Row 的直接子组件,生效
      child: Container(child: Text("会拉伸")),
    ),
  ],
)

问题3:flex值为0时效果

  • flex: 0表示不分配剩余空间,子组件仅占自身大小,等同于不使用Expanded(适用于“按需拉伸”的场景)
Row(
  children: [
    Expanded(flex: 0, child: Container(width: 100, color: Colors.red)), // 仅占 100px 宽度
    Expanded(flex: 1, child: Container(color: Colors.blue)), // 占剩余空间
  ],
)

三、层叠布局:Stack + Positioned

1. 核心组件关系

  • Stack:层叠布局的容器,子组件按“添加顺序”堆叠(后添加的组件在上面),支持两种子组件:
    • 普通子组件:默认按Stack的alignment属性对齐(如:居中、左上角)。
    • Positioned子组件:通过left/right/top/bottom精准控制位置,必须是Stack的直接子组件。
  • Positioned:仅能在Stack中使用,用于“固定位置”,通过左边控制子组件在Stack中的位置和大小。

2. Stack组件核心属性

属性名作用常用值
alignment普通子组件的默认对齐方式Alignment.center(默认居中)、Alignment.topLeft、Alignment.bottomRight等
fit普通子组件的大小适配方式

StackFit.loose(默认,子组件按自身大小)、

StackFit.expand(子组件占满Stack大小)

overflow子组件超出Stack范围时的处理方式

Overfow.clip(默认,裁剪超出部分)、

Overflow.visible(显示超出部分)

children子组件列表(可包含普通组件和Positioned)任意Widget组合

3. Positioned组件核心属性 

属性名作用说明
left/right子组件距离Stack左侧/右侧的距离二选一(或都不设,自动适配宽度)
top/bottom子组件距离Stack顶部/底部的距离二选一(或都不设,自动适配高度)
width/height子组件的固定宽高若设置,会覆盖left + right / top + bottom计算的大小
child被定位的子组件任意Widget(如:Text、Container)

4. 示例代码

示例1:基础层叠(普通子组件 + 默认对齐)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Stack 基础示例'),),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Stack(
            alignment: Alignment.center,
            children: [
              Container(
                width: double.infinity,
                height: 200,
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: const NetworkImage('https://upyun.askrabbit.net/avatar.png'),
                    fit: BoxFit.cover
                  )
                ),
              ),
              const Text(
                '这是一张图片的标题',
                style: TextStyle(
                  color: Colors.yellow,
                  fontSize: 22,
                  fontWeight: FontWeight.bold,
                  shadows: [Shadow(color: Colors.black, blurRadius: 3)]
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
  • 运行效果

示例2:Positioned固定位置(精准定位)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Positioned定位示例'),),
        body: Padding(
          padding: EdgeInsets.all(16),
          child: Stack(
            children: [
              Container(
                width: double.infinity,
                height: 200,
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: const NetworkImage('https://upyun.askrabbit.net/avatar.png'),
                    fit: BoxFit.cover
                  )
                ),
              ),
              Positioned(
                top: 10, // 距离顶部10px
                right: 10, // 距离右侧10px
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  color: Colors.red,
                  child: const Text('热门', style: TextStyle(color: Colors.white, fontSize: 12),),
                ),
              ),
              Positioned(
                left: 0,
                right: 0,
                bottom: 0,
                child: Container(
                  padding: const EdgeInsets.all(12),
                  decoration: const BoxDecoration(
                    gradient: LinearGradient(
                      begin: Alignment.bottomCenter,
                      end: Alignment.topCenter,
                      colors: [Color(0x80000000), Color(0x00000000)],
                    )
                  ),
                  child: const Text(
                    '风景图片展示',
                    style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
  • 运行效果

示例3:Stack溢出处理(clipBehavior 属性)

当子组件超出Stack范围时,默认会被裁剪(Clip.hardEdge),可通过clipBehavior : Clip.none显示超出部分(针对Flutter 3.10+版本)

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text("Stack 溢出处理")),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Stack(
            clipBehavior: Clip.none, // 显示超出部分
            children: [
              // 底层:白色容器(固定大小)
              Container(
                width: 200,
                height: 200,
                decoration: const BoxDecoration(
                  color: Colors.white, // 背景色
                  boxShadow: [
                    BoxShadow(color: Colors.grey, blurRadius: 3) // 阴影
                  ]
                )
              ),
              // 上层:红色容器(超出底层范围)
              Positioned(
                top: -20, // 向上超出 20px
                left: -20, // 向左超出 20px
                child: Container(width: 60, height: 60, color: Colors.red, child: const Center(child: Text("NEW", style: TextStyle(color: Colors.white)))),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
  • 运行效果
  • 适用场景:需要实现“悬浮标签”,“气泡提示”等超出父组件范围的UI。

示例4:Stack + Expanded组合(自适应层叠)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Stack + Expanded组合'),),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              const Text('自适应层叠布局', style: TextStyle(fontSize: 18),),
              const SizedBox(height: 16,),
              Expanded(
                child: Stack(
                  children: [
                    Container(
                      color: Colors.yellow[100], 
                    ),
                    const Center(
                        child: Text('自适应重叠', style: TextStyle(fontSize: 24),),
                    ),
                    Positioned(
                      bottom: 16,
                      right: 16,
                      child: ElevatedButton(onPressed: () {}, child: const Text('点击按钮')),
                    )
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
  • 运行效果

5. 层叠布局常见问题与解决方案

问题1:Positioned定位失效

  • 原因1:Positioned不是Stack的直接子组件(如:嵌套在Container中)。
  • 原因2:同时设置了left和right但未设置width,或者同时设置了top和bottom但未设置height(导致大小冲突)。

问题2:Stack子组件无法居中(普通子组件)

  • 原因:Stack的alignment属性设置错误,或子组件被Positioned包裹(Positioned会忽略alignment)。

问题3:Stack溢出警告

  • 现象:子组件超出Stack范围你,出现黄色警告条。
  • 解决方案:
    • 扩大Stack大小(如:用Expanded让Stack占满空间)。
    • 设置clipBehavior : Clip.none显示超出部分。
    • 调整子组件大小或Positioned的坐标

四、综合实战

  • 构建一个电商App常见的“商品卡片”,包含以下元素:
    • 商品图片(占满卡片宽度,固定高度)。
    • 右上角“折扣标签”(层叠布局,Positioned定位)。
    • 商品标题、价格(弹性布局,标题占满剩余空间,价格右对齐)。
    • 底部“加入购物车”按钮(占满卡片宽度)。
  • 代码参考
import 'package:flutter/material.dart';

void main() => runApp(App());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('商品信息列表'),),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: SingleChildScrollView(
            child: Column(
              children: [
                // 商品卡片(固定宽度,可复用)
                _productCard(),
                const SizedBox(height: 16,),
                _productCard()
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// 商品卡
Widget _productCard() {
  return Container(
    width: double.infinity, // 占满父组件宽度
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: Colors.yellow,
      borderRadius: BorderRadius.circular(12),
      boxShadow: [BoxShadow(color: Colors.grey.withValues(alpha: 0.2), blurRadius: 6, offset: Offset(0, 2))],
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start, // 子组件左对齐
      children: [
        // 1. 商品图片 + 折扣标签(层叠布局)
        Stack(
          children: [
            // 商品图片
            Container(
              width: double.infinity,
              height: 180,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(8),
                image: DecorationImage(
                  image: const NetworkImage("https://picsum.photos/id/26/600/400"),
                  fit: BoxFit.cover
                )
              ),
            ),
            // 折扣标签(右上角定位)
            Positioned(
              top: 10,
              right: 10,
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                color: Colors.red,
                child: const Text('8折', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),),
              ),
            ),
          ],
        ),
        const SizedBox(height: 12,),
        // 2. 商品标题 + 价格(弹性布局)
        Row(
          children: [
            Expanded(
              child: const Text(
                '2025新款冬季羽绒服',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
                maxLines: 1, // 最多显示1行
                overflow: TextOverflow.ellipsis, // 超出部分用...表示
              ),
            ),
            const SizedBox(width: 8,),
            // 价格:右对齐,不拉伸
            const Text(
              '¥99',
              style: TextStyle(fontSize: 18, color: Colors.red, fontWeight: FontWeight.bold),
            )
          ],
        ),
        const SizedBox(height: 8,),
        // 3. 原价
        const Text(
          '原价:¥129',
          style: TextStyle(fontSize: 14, color: Colors.grey, decoration: TextDecoration.lineThrough),
        ),
        const SizedBox(height: 12,),
        // 4. 加入购物车按钮(占满宽度)
        ElevatedButton(
          onPressed: () {},
          style: ElevatedButton.styleFrom(
            minimumSize: const Size(double.infinity, 44), // 占满宽度,高度是44
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
          ),
          child: const Text('加入购物车', style: TextStyle(fontSize: 16),),
        )
      ],
    ),
  );
}
  • 运行效果


目录