Flutter-详解布局(响应式和平台适配及特殊布局控件)

DEMO

平台适配布局控件

1. SafeArea

  • 说明: 用于规避系统 UI(如状态栏、刘海屏、底部导航栏)的遮挡。它会根据设备屏幕的“安全区域”自动添加内边距(padding),确保内容不被系统控件覆盖,使 UI 动态且适应各种设备。在设计组件布局时,我们考虑了不同设备及其屏幕占用的约束,如状态栏、刘海屏、导航栏等。然而,新设备不断推出,设计各异,有时你的应用可能会覆盖这些占用的约束。因此,为了使 UI 适应性强且无错误,我们使用 SafeArea 组件。SafeArea 实际上是一个填充组件,根据设备运行时的情况,为你的应用添加必要的填充。如果应用的组件覆盖了系统特征,如刘海屏、状态栏、相机孔等,SafeArea 会根据需要在周围添加填充。SafeArea 内部使用 MediaQuery 检查屏幕尺寸,并在必要时包含额外填充。构造函数允许你决定是否在特定方向上避免侵入,通过布尔值 true 或 false 来实现

  • 规则: 在需要避免系统侵入的区域包裹 SafeArea 组件,通过 left、top、right、bottom 属性控制是否在相应方向上避免系统侵入;通过 minimum 属性设置最小填充;通过 maintainBottomViewPadding 属性决定是否保持底部填充不变,将需要保护的子组件放在 SafeArea 的 child 属性中

  • 注意: 当软键盘弹出时,底部填充可能会消失。可以通过设置 maintainBottomViewPadding 为 true 来保持底部填充不变,在页面骨架中直接应用,避免全局遮挡,在沉浸式全屏(如视频播放页)中需关闭 SafeArea,否则顶部留白,若父容器已设置 padding(如 ListView),SafeArea 可能造成双重边距。此时需手动调整,强制取消安全边距(如 padding: EdgeInsets.zero )可能导致内容被遮挡

  • 推荐: 顶部状态栏区域、底部导航栏区域、刘海屏/挖孔屏设备、弹窗/浮层内容

SafeArea(
  // 2. 关闭底部安全区域(自定义底部栏时使用)
  bottom: false,
  child: Column(
    children: [
      // 3. 顶部标题栏(自动避开状态栏)
      const AppHeader(),
      // 4. 内容区域(使用Expanded填充剩余空间)
      Expanded(
        child: ListView(
          children: List.generate(
              20, (i) => ListTile(title: Text("Item $i"))),
        ),
      ),
      // 5. 自定义底部导航栏(需手动避开系统栏)
      const CustomBottomBar(),
    ],
  ),
)
        
// 自定义顶部组件 
class AppHeader extends StatelessWidget {
  const AppHeader({super.key}); 
 
  @override 
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue, 
      padding: const EdgeInsets.all(16), 
      child: const Text("SafeArea Demo", style: TextStyle(color: Colors.white)), 
    );
  }
}
 
// 自定义底部组件(需单独处理安全边距)
class CustomBottomBar extends StatelessWidget {
  const CustomBottomBar({super.key}); 
 
  @override
  Widget build(BuildContext context) {
    // 6. 为底部栏单独添加SafeArea 
    return SafeArea(
      top: false, // 关闭顶部边距
      minimum: const EdgeInsets.only(bottom:  10), // 追加额外外边距
      child: Container(
        height: 50,
        color: Colors.green, 
        child: const Center(child: Text("Bottom Bar")),
      ),
    );
  }
}
SafeArea

2. PlatformView

  • 说明: 允许在 Flutter 应用中嵌入原生的 UI 组件,如 Android 的 View 或 iOS 的 UIView

  • 规则:

  1. 首先需要在原生代码中创建一个自定义的 PlatformView。
  2. 创建一个 PlatformViewFactory 来创建 PlatformView 实例,并将其与 Flutter 应用关联
  3. 通过 PlatformView Widget 将原生视图嵌入到 Flutter 应用中
  4. 通过 MethodChannel 实现 Dart 与原生代码的双向通信
  • 注意: 由于 PlatformView 是原生视图,频繁的交互可能会影响性能,因此应尽量减少不必要的原生视图嵌入,确保正确管理原生视图的生命周期,避免内存泄漏,不同平台的原生视图实现方式不同,需要分别处理 Android 和 iOS 的实现,Dart 的 viewType(native_text_view)需与原生注册的 ID 匹配

  • 推荐: 嵌入地图、视频播放、相机、文件选择器、WebView、条形码扫描

// dart
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    // Flutter 文本显示原生视图传回的时间
    Text('Flutter 显示: $_currentTime',
        style: const TextStyle(fontSize: 20)),
    const SizedBox(height: 30),
    // 原生视图容器
    Container(
      width: 300,
      height: 200,
      decoration: BoxDecoration(
        border: Border.all(color: Colors.blue, width: 2),
        borderRadius: BorderRadius.circular(10),
      ),
      child: Platform.isAndroid
          ? const AndroidView(
              viewType: _viewType,
              creationParams: {'textColor': '#FF0000'}, // 红色文本
              creationParamsCodec: StandardMessageCodec(),
            )
          : const UiKitView(
              viewType: _viewType,
              creationParams: {'textSize': 24.0}, // iOS 文本大小
              creationParamsCodec: StandardMessageCodec(),
            ),
    ),
  ],
)

// swift
// TimeView.swift
class TimeView: NSObject, FlutterPlatformView {
    private var label: UILabel
    private var methodChannel: FlutterMethodChannel?
    
    init(frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, messenger: FlutterBinaryMessenger) {
        label = UILabel(frame: frame)
        label.textAlignment = .center
        label.text = "iOS原生时间视图"
        
        super.init()
        
        // 解析Flutter传递的参数
        if let params = args as? [String: Any], 
           let textSize = params["textSize"] as? Double {
            label.font = UIFont.systemFont(ofSize: CGFloat(textSize))
        } else {
            label.font = UIFont.systemFont(ofSize: 20)
        }
        
        label.textColor = .blue
        
        // 创建方法通道
        methodChannel = FlutterMethodChannel(
            name: "com.example/time_channel", 
            binaryMessenger: messenger
        )
        
        methodChannel?.setMethodCallHandler(handleMethodCall)
    }
    
    func view() -> UIView {
        return label
    }
    
    func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getCurrentTime":
            // 返回当前时间给Flutter
            let formatter = DateFormatter()
            formatter.dateFormat = "HH:mm:ss"
            result(formatter.string(from: Date()))
        default:
            result(FlutterMethodNotImplemented)
        }
    }
}

// TimeViewFactory.swift
class TimeViewFactory: NSObject, FlutterPlatformViewFactory {
    private var messenger: FlutterBinaryMessenger

    init(messenger: FlutterBinaryMessenger) {
        self.messenger = messenger
        super.init()
    }

    func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
        return FlutterStandardMessageCodec.sharedInstance()
    }

    func create(
        withFrame frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?
    ) -> FlutterPlatformView {
        return TimeView(
            frame: frame,
            viewIdentifier: viewId,
            arguments: args,
            messenger: messenger
        )
    }
}

// AppDelegate.swift (添加部分)

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        
        // 注册PlatformView
        let factory = TimeViewFactory(messenger: controller.binaryMessenger)
        registrar(forPlugin: "TimeViewPlugin")?.register(
            factory,
            withId: "com.example/NativeTimeView"
        )
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

// ios 配置
<!-- ios/Runner/Info.plist -->
<key>io.flutter.embedded_views_preview</key>
<true/>

// android

// TimeView.kt
package com.example.platformviewdemo;

import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.platform.PlatformView;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;

class TimeView implements PlatformView, MethodChannel.MethodCallHandler {
    private final TextView textView;
    private final MethodChannel methodChannel;

    TimeView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
        textView = new TextView(context);
        
        // 解析Flutter传递的参数
        if (params != null && params.containsKey("textColor")) {
            String color = (String) params.get("textColor");
            textView.setTextColor(Color.parseColor(color));
        } else {
            textView.setTextColor(Color.BLUE);
        }
        
        textView.setTextSize(20);
        textView.setText("Android原生时间视图");
        
        // 创建方法通道
        methodChannel = new MethodChannel(messenger, "com.example/time_channel");
        methodChannel.setMethodCallHandler(this);
    }

    @Override
    public View getView() {
        return textView;
    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        if (call.method.equals("getCurrentTime")) {
            // 返回当前时间给Flutter
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
            result.success(sdf.format(new Date()));
        } else {
            result.notImplemented();
        }
    }

    @Override
    public void dispose() {
        methodChannel.setMethodCallHandler(null);
    }
}

// TimeViewFactory.kt
package com.example.platformviewdemo;

import android.content.Context;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.util.Map;

public class TimeViewFactory extends PlatformViewFactory {
    private final BinaryMessenger messenger;

    public TimeViewFactory(BinaryMessenger messenger) {
        super(StandardMessageCodec.INSTANCE);
        this.messenger = messenger;
    }

    @Override
    public PlatformView create(Context context, int viewId, Object args) {
        Map<String, Object> params = (Map<String, Object>) args;
        return new TimeView(context, messenger, viewId, params);
    }
}

// android/app/src/main/java/com/example/platformviewdemo/MainActivity.kt
package com.example.platformviewdemo;

import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "com.example/time_channel";

    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);
        
        // 注册PlatformView
        flutterEngine
            .getPlatformViewsController()
            .getRegistry()
            .registerViewFactory(
                "com.example/NativeTimeView", 
                new TimeViewFactory(flutterEngine.getDartExecutor())
            );
    }
}



AndroidView: 嵌入Android原生视图

UiKitView: 嵌入iOS原生视图

平台适配:

Platform.isIOS
  ? CupertinoButton(onPressed: () {}, child: Text('iOS'))
  : ElevatedButton(onPressed: () {}, child: Text('Android'));
PlatformView

特殊布局控件

1. Offstage

  • 说明: 用于控制子组件是否参与布局和渲染的一个小部件,其核心功能是通过offstage属性切换子组件的可见状态

  • 规则: 当 offstage 属性设置为 true 时,子组件会被隐藏,并且不会参与布局或渲染,也不会占用任何空间,当 offstage 为 false 时,子组件正常显示

  • 注意: 如果子组件有动画,应该手动停止动画,因为 Offstage 不会停止动画,Offstage 不会从渲染树中移除子组件,只是不绘制和不响应点击事件,因此在需要频繁切换显示状态的场景中,Offstage 是一个高效的选择,隐藏状态的子组件不占用空间,可能导致父布局重新计算尺寸(如Column中的子组件隐藏后,其他子组件会向上移动)。需注意布局的稳定性,避免频繁切换导致界面抖动

  • 推荐: 实现平滑的显示/隐藏动画(如淡入淡出、滑动入场)

bool _isHidden = false; // 控制文本是否隐藏

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    // 使用 Offstage 控制文本显示
    Offstage(
      offstage: _isHidden,
      child: const Text(
        'Hello, Offstage!',
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      ),
    ),
    const SizedBox(height: 20),
    // 切换按钮
    ElevatedButton(
      onPressed: () {
        setState(() => _isHidden = !_isHidden);
      },
      child: Text(_isHidden ? '显示文本' : '隐藏文本'),
    ),
  ],
)
Offstage

2. Visibility

  • 说明: 用于控制子组件的可见性。当设置为“可见”时,子组件将显示;设置为“隐藏”时,子组件将被隐藏。该组件还提供了多个可选属性来控制子组件的状态和行为,例如是否维持状态、动画、大小和交互性

  • 规则:

  1. child: 必填属性,指定要控制可见性的子组件。
  2. visible: 可选属性,布尔值,控制子组件的显示或隐藏,默认为true。
  3. replacement: 可选属性,当子组件不可见时显示的替代小部件。
  4. maintainState: 可选属性,布尔值,控制子组件状态是否保持不变,默认为false。
  5. maintainAnimation: 可选属性,布尔值,控制子组件动画是否保持不变,默认为false。
  6. maintainSize: 可选属性,布尔值,控制子组件空间是否保持不变,默认为false。
  7. maintainInteractivity: 可选属性,布尔值,控制子组件在不可见时是否仍可交互,默认为false。
  8. maintainSemantics: 可选属性,布尔值,控制子组件语义是否保持不变,默认为false。
  • 注意: 即使子组件被隐藏,其状态也会被维持,这在需要保留用户输入或状态的情况下非常有用,对高频切换显隐的组件(如列表项),避免启用 maintainState,防止内存泄漏,

  • 推荐: 表单动态字段、Tab 切换内容、配合 AnimatedSwitcher 实现渐变/缩放效果

Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Visibility(
        visible: _isShown,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.amber,
        ),
      ),
      const SizedBox(height: 20),
      ElevatedButton(
        onPressed: _toggleVisibility,
        child: Text(_isShown ? '隐藏盒子' : '显示盒子'),
      ),
    ],
  ),
)
Visibility

3. IgnorePointer

  • 说明: 用于忽略其子组件的指针事件(如点击、拖动等)。这意味着包裹在 IgnorePointer 中的子组件将不会响应用户的交互操作,但仍然会显示在界面上。IgnorePointer 与 AbsorbPointer 类似,但 IgnorePointer 不会终止指针事件,而是让这些事件传递到其下方的组件

  • 规则: ignoring: 布尔值,决定是否忽略指针事件。默认为 true,ignoringSemantics: 布尔值,决定是否忽略语义信息。默认为 null,即不忽略,可以嵌套在任何布局组件(如 Stack、Column)中。
    若多个 IgnorePointer 嵌套,外层 ignoring 为 true 时,内层设置无效,事件穿透:当 ignoring 为 true 时,子组件的点击事件会穿透到父级组件。适用于需要隐藏交互但保留显示效果的场景(如背景图、装饰元素)

  • 注意: 避免在复杂嵌套结构中滥用,可能导致不必要的渲染开销。优先结合 Stack 和 Positioned 实现局部交互控制,需动态控制 ignoring 时,建议通过 StatefulWidget 管理状态,配合 setState 更新,与 GestureDetector 结合使用时,需注意事件优先级。例如,外层 IgnorePointer 为 true 时,内层 GestureDetector 无法触发事件

  • 推荐: 背景图片、水印等无需交互的组件、表单提交时禁用按钮点击,防止重复提交、在 Stack 中叠加多个组件时,控制特定层的交互优先级

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    // 使用 IgnorePointer 包裹按钮
    IgnorePointer(
      ignoring: _isIgnoring,
      child: ElevatedButton(
        onPressed: () {
          print('按钮被点击');
        },
        child: const Text('可点击按钮'),
      ),
    ),
    const SizedBox(height: 20),
    // 切换开关
    Switch(
      value: _isIgnoring,
      onChanged: (value) {
        setState(() {
          _isIgnoring = value;
        });
      },
      activeTrackColor: Colors.blueGrey,
      activeColor: Colors.blue,
    ),
    const Text('开关开启时按钮不可点击'),
  ],
)
IgnorePointer

4. AbsorbPointer

  • 说明: 用于阻止子组件接收指针事件的布局组件,其核心功能是通过终止命中测试(HitTest)来禁用子树的交互能力。它不会影响布局和绘制,仅控制事件传递

  • 规则: absorbing:布尔值,默认为 true。当设置为 true 时,子组件将无法接收用户输入事件,必须直接包裹需要禁用的子组件,且不影响父级或外部组件的交互

  • 注意: 虽然 AbsorbPointer 的使用不会显著影响性能,但在复杂的布局中应谨慎使用,避免不必要的嵌套,需配合状态管理(如 setState)动态控制 absorbing 属性,实现交互开关,即使 AbsorbPointer 设置为吸收模式,它仍然会将点击事件传递给其父组件。这意味着父组件的 GestureDetector 仍然能够捕获点击事件

  • 推荐: 临时禁用交互、批量禁用组件

Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      // 外层可点击区域
      GestureDetector(
        // ignore: avoid_print
        onTap: () => print('外层点击'),
        child: Container(
          color: Colors.grey,
          padding: const EdgeInsets.all(16),
          child: AbsorbPointer(
            absorbing: true, // 禁用子组件交互
            child: Row(
              children: [
                ElevatedButton(
                  // ignore: avoid_print
                  onPressed: () => print('按钮点击'),
                  child: const Text('禁用按钮'),
                ),
                const SizedBox(width: 16),
                const Text('不可点击区域'),
              ],
            ),
          ),
        ),
      ),
      const SizedBox(height: 20),
      // 对比示例(交互正常)
      GestureDetector(
        // ignore: avoid_print
        onTap: () => print('外层点击'),
        child: Container(
          color: Colors.grey,
          padding: const EdgeInsets.all(16),
          child: AbsorbPointer(
            absorbing: false, // 启用子组件交互
            child: ElevatedButton(
              // ignore: avoid_print
              onPressed: () => print('正常按钮'),
              child: const Text('可点击按钮'),
            ),
          ),
        ),
      ),
    ],
  ),
)
AbsorbPointer

Overlay 覆盖层

Opacity 透明度效果

AnimatedOpacity 动画透明度

ClipRRect 圆角裁剪

ClipOval 椭圆裁剪

ClipPath 路径裁剪

ClipRect 矩形裁剪

BackdropFilter 背景滤镜

DecoratedBox 装饰盒子

RotatedBox 旋转盒子

平台特定布局控件

CupertinoPageScaffold iOS风格页面脚手架

CupertinoTabScaffold iOS风格标签栏脚手架

CupertinoNavigationBar iOS风格导航栏

CupertinoTabBar iOS风格标签栏

CupertinoActionSheet iOS风格操作表

CupertinoAlertDialog iOS风格警告对话框

CupertinoContextMenu iOS风格上下文菜单

MaterialApp Material Design 应用容器

Scaffold Material Design 页面脚手架

AppBar Material Design 应用栏

BottomAppBar Material Design 底部应用栏

TabBar Material Design 标签栏

Drawer Material Design 抽屉

SnackBar Material Design 底部消息条

BottomSheet Material Design 底部表单

Material Material Design 表面

Card Material Design 卡片

Chip Material Design 标签

Divider Material Design 分割线: 水平分割线组件,常见于列表项、板块划分等场景

ListTile Material Design 列表项: 主要用于创建列表项(List Item),通常用于 ListView、Card 或 Drawer 等布局中

总结

虽然 Flutter 提供了超过 100+ 个布局相关控件,以上列举覆盖了几乎所有官方核心布局控件,实际开发中最常用的约只有 15 个。掌握 Row/Column/Stack/Expanded/ListView这五个核心组件即可解决绝大多数布局需求。

基础布局三巨头: Row/Column(线性布局)、Stack(层叠布局)、ListView/GridView(滚动布局)满足 80% 场景

弹性布局必用: Expanded 和 Flexible 是自适应布局的核心

性能关键: 长列表必用 ListView.builder,避免滥用 IntrinsicWidth/Height,复杂动画使用 CustomMultiChildLayout

响应式最佳实践: LayoutBuilder + MediaQuery + 约束条件判断

因为网站字数限制,只能分系列了,需要一次性看完请去这里
Flutter-详解布局(核心布局控件)
Flutter-详解布局(单子组件布局控件)
Flutter-详解布局(弹性和层叠布局辅助控件)
Flutter-详解布局(滚动和Sliver系列布局控件)
需要代码去这里DEMO

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容