30天计划第15天-跨端适配基础

-
-
2025-11-20 12:21
  • 学习内容

    1. 屏幕适配:MediaQuery 获取屏幕宽高,按比例设置组件大小

    2. 平台判断:Platform.isAndroid/Platform.isIOS(区分安卓 /iOS 样式)

  • 实践任务

    1. 做一个 “设置页面”:安卓显示 “返回键”,iOS 显示 “完成键”(用平台判断)

    2. 按钮宽高按屏幕比例设置(如宽度 = 屏幕宽 * 0.8)

注意:Flutter Web不支持使用dart.io(包含文件、网络、系统信息等原生API)。

一、理论基础:屏幕适配与平台判断

1. 屏幕适配:MediaQuery

为什么需要屏幕适配?

        Flutter应用可能运行在各种尺寸和分辨率的设备上(手机、平板、手表等),如果硬编码组件的尺寸(如:width: 300.0),在大屏幕上可能显得很小,在小屏幕上可能超出边界。屏幕适配就是让UI在不同设备上都能保持合理的布局和美观的比例。

使用MediaQuery获取屏幕信息

        MediaQuery是一个可以获取当前设备屏幕信息的工具类。它可以用来获取屏幕的宽、高、像素密度、方向等。

  • 核心代码

    // 在 build 方法中获取 MediaQueryData
    final mediaQueryData = MediaQuery.of(context);
    
    // 获取屏幕宽高(逻辑像素)
    final screenWidth = mediaQueryData.size.width;
    final screenHeight = mediaQueryData.size.height;
    
    // 获取设备像素比
    final pixelRatio = mediaQueryData.devicePixelRatio;
    
    // 获取状态栏高度
    final statusBarHeight = mediaQueryData.padding.top;
    
    // 获取屏幕方向
    final orientation = mediaQueryData.orientation; // Orientation.portrait 或 Orientation.landscape
  • 按比例设置组件大小

    这是最常用也是最简单的适配方法。如:希望一个按钮的宽度是屏幕宽度的80%,高度是屏幕高度的6%,代码如下:

    Widget build(BuildContext context) {
      final screenWidth = MediaQuery.of(context).size.width;
      final screenHeight = MediaQuery.of(context).size.height;
    
      return Scaffold(
        body: Center(
          child: Container(
            // 宽度 = 屏幕宽度 * 0.8
            width: screenWidth * 0.8,
            // 高度 = 屏幕高度 * 0.06
            height: screenHeight * 0.06,
            color: Colors.blue,
            child: const Center(child: Text("自适应按钮")),
          ),
        ),
      );
    }
  • 优点
    • 实现简单,代码直观。
    • 能快速适配大部分场景,保持UI元素的相对比例。
  • 缺点
    • 对于复杂的UI,纯比例适配可能不够惊喜,某些组件可能需要更灵活的调整。这时可能需要结合LayoutBuilder或响应式框架(如:flutter_screenutil)。

2. 平台判断:Platform

为什么需要平台判断?

        Android和iOS平台在设计规范和用户习惯上存在差异,为了提供更贴近原生的用户体验,有时需要为不同平台设计略有不同的UI:

  • 导航栏:Android的返回键通常在左上角,而iOS的“完成”或“取消”按钮在右上角。
  • 表单控件:开关(Switch)、滑块(Slider)等组件在两个平台上的默认样式不同。
  • 页面跳转:Android通常是从右向左滑入,iOS是从下向上滑入(模态窗口)。

使用dart: io中的Platform类

        Platform类提供了判断当前操作系统的方法。

  • 核心代码

    import 'dart:io';
    
    if (Platform.isAndroid) {
      // 安卓平台的逻辑
      print('Running on Android');
    } else if (Platform.isIOS) {
      // iOS 平台的逻辑
      print('Running on iOS');
    } else if (Platform.isWindows) {
      // Windows 平台的逻辑
    } else if (Platform.isMacOS) {
      // macOS 平台的逻辑
    } else if (Platform.isLinux) {
      // Linux 平台的逻辑
    } else if (Platform.isFuchsia) {
      // Fuchsia 平台的逻辑
    }

    注意:Platform判断的是应用运行的操作系统,不是编译时的目标平台。

二、综合实战

  • 任务需求:制作自适应设置页面
    • 页面结构
      • 一个标准的Scaffold。
      • AppBar的标题为“设置”。
      • AppBar的leading(左侧)或actions(右侧)按钮根据平台显示:
        • Android:在leading位置显示一个“返回”图标按钮(Icon(Icon.arrow_back))。
        • iOS:在actions位置显示一个“完成”文本按钮(Text(“完成”))。
      • body为一个ListView,包含若干设置项
    • UI适配
      • AppBar上的按钮(返回/完成)的padding或minWidth按屏幕比例设置,使其在不同尺寸屏幕上大小合适。
      • ListView中的设置项(如:一个模拟的“清除缓存”按钮),其宽度为屏幕宽度的80%,居中显示,高度也按比例设置。
  • 代码实现

    import 'package:flutter/material.dart';
    import 'dart:io' as io;
    import 'dart:math';
    import 'dart:async';
    import 'package:flutter/foundation.dart' show kIsWeb;
    
    
    void main() {
      AppPlatform.initMockPlatform();
      runApp(const App());
    }
    
    
    class AppPlatform {
      static String? _mockPlatform;
    
      static void initMockPlatform() {
        if (kIsWeb) {
          _mockPlatform = 'ios';
        }
      }
    
      static bool get isAndroid {
        if (kIsWeb) {
          return _mockPlatform == 'android';
        }
        return io.Platform.isAndroid;
      }
    
      static bool get isIOS{
        if (kIsWeb) {
          return _mockPlatform == 'ios';
        }
        return io.Platform.isIOS;
      }
    }
    
    class App extends StatelessWidget {
      const App({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue
          ),
          home: const SettingsPage(),
        );
      }
    }
    
    class SettingsPage extends StatefulWidget {
      const SettingsPage({super.key});
    
      @override
      State<SettingsPage> createState() => _SettingsPageState();
    }
    
    class _SettingsPageState extends State<SettingsPage>{
      bool _notificationsEnabled = true;
      double _cacheSize = 2.5;
      Timer? _cacheGrowthTimer;
      final Random _random = Random();
    
      @override
      void initState() {
        super.initState();
        _startNextRandomGrowthTimer();
      }
    
      @override
      void dispose() {
        _cacheGrowthTimer?.cancel();
        super.dispose();
      }
    
      void _startNextRandomGrowthTimer() {
        int randomSeconds = _random.nextInt(26) + 5;
        Duration randomDuration = Duration(seconds: randomSeconds);
    
        _cacheGrowthTimer = Timer(randomDuration, (){
          _growCache();
          _startNextRandomGrowthTimer();
        });
      }
    
      void _growCache() {
        setState(() {
          double growth = _random.nextDouble() * 0.4 + 0.1;
          _cacheSize += growth;
          _cacheSize = double.parse(_cacheSize.toStringAsFixed(1));
        });
      }
    
      void _onBackOrDonePressed() {
        Navigator.of(context).pop();
      }
    
      void _onClearCachePressed() {
        // 模拟清除缓存
        setState(() {
          _cacheSize = 0.0;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('缓存已清除')),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        // 获取屏幕尺寸
        final screenWidth = MediaQuery.of(context).size.width;
        final screenHeight = MediaQuery.of(context).size.height;
    
        // 定义自适应尺寸
        final buttonWidth = screenWidth * 0.8;
        final buttonHeight = screenHeight * 0.06;
    
        return Scaffold(
          appBar: AppBar(
            title: const Text('设置'),
            centerTitle: true,
            leading: AppPlatform.isAndroid
             ? IconButton(onPressed: _onBackOrDonePressed, icon: const Icon(Icons.arrow_back))
             : null,
            actions: AppPlatform.isIOS
              ? [
                TextButton(
                  onPressed: _onBackOrDonePressed,
                  child: const Text(
                    '完成',
                    style: TextStyle(color: Colors.white, fontSize: 16),
                  ),
                )
              ]
              : null,
          ),
          body: ListView(
            padding: EdgeInsets.symmetric(vertical: screenHeight * 0.02),
            children: [
              SwitchListTile(
                title: const Text('启用通知'),
                subtitle: const Text('接收应用通知'),
                value: _notificationsEnabled,
                onChanged: (bool value){
                  setState(() {
                    _notificationsEnabled = value;
                  });
                }
              ),
              const Divider(height: 1,),
              ListTile(
                title: const Text('主题'),
                subtitle: const Text('深色模式'),
                trailing: const Icon(Icons.chevron_right),
                onTap: () {
                  // 跳转主题设置页面
                },
              ),
              const Divider(height: 1,),
              Padding(
                padding: EdgeInsets.symmetric(
                  horizontal: screenWidth * 0.1,
                  vertical: screenHeight * 0.01
                ),
                child: SizedBox(
                  width: buttonWidth,
                  height: buttonHeight,
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.grey[200],
                      foregroundColor: Colors.black87,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8)
                      )
                    ),
                    onPressed: _onClearCachePressed,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text('清除缓存'),
                        Text('${_cacheSize.toStringAsFixed(1)}MB'),
                      ],
                    )
                  ),
                ),
              )
            ],
          ),
        );
      }
    }
  • 运行效果

  • 说明:
    • 代码中增加了随机增加缓存模拟,在5~30s内随机小幅度增加0.1~0.5M缓存。
    • 如果在web平台运行,Platform将会报错,这是因为dart.io不支持在Flutter Web中使用,因此示例代码中做了模拟封装。

      class AppPlatform {
        static String? _mockPlatform;
      
        static void initMockPlatform() {
          if (kIsWeb) {
            _mockPlatform = 'ios'; // 模拟ios系统
          }
        }
      
        static bool get isAndroid {
          if (kIsWeb) {
            return _mockPlatform == 'android';
          }
          return io.Platform.isAndroid;
        }
      
        static bool get isIOS{
          if (kIsWeb) {
            return _mockPlatform == 'ios';
          }
          return io.Platform.isIOS;
        }
      }

 


目录