Flutter:使用图像作为屏幕背景

我本来想用渐变色 做屏幕背景,结果它给我报了个"shader 编译时间太长"的错误 (或警告?)。主要是我的安卓设备 GPU 性能太弱了

所以我就开始琢磨:"干脆直接用图片来做背景,不是更好吗?"


关于性能(Performance)

很明显,我们不能用那种又大、分辨率又高 的图片来做背景。下面我给的两个例子,用的都是 10KB 大小的 WebP 格式图片

从 assets 里加载一个 10KB 的 WebP 图,对性能的影响可以忽略不计 ,尤其如果我们在应用启动时就把它预加载好的话。

不过,第一次加载时可能还是会有一点感觉 ,所以我们最好把 ColorScheme 里的 surface color (表面颜色)设置成跟背景图的主色调接近


看看例子(Examples)

这里有两个注册页面的例子,分别展示了在浅色和深色的图片背景下的效果。

ColorScheme (配色方案)是由 ChatGPT 生成 的。一个是为了 surface color (表面颜色)是 pink.shade100 ,第二个是为了 surface colorgrey.shade800

有点太花哨了 (或者说太活泼了 )不合我的口味,但就像我说的,这得怪 ChatGPT 。而且,这里的重点不是按钮的颜色背景 才是重点。背景看起来效果不错

显然,我们应该把图片放到 assets 文件夹里:

并且在 pubspec.yaml 文件中提及路径

我是从 Canva 获取我的图片的,但其实你可以随便找一张照片 ,给它做个模糊效果加一个半透明图层 ,然后以中等质量 保存成 WebP 格式就行了。

以下就是本文的主要核心代码BgScaffold

dart 复制代码
import 'package:flutter/material.dart';
import 'package:getx_miscellanous/app/data/memory_settings_service.dart';

class BgScaffold  extends StatelessWidget {
  final Widget? body;
  final PreferredSizeWidget? appBar;
  final Widget? floatingActionButton;
  final FloatingActionButtonLocation? floatingActionButtonLocation;
  final Widget? bottomNavigationBar;
  final Widget? drawer;
  final Widget? endDrawer;
  final String? lightBackgroundImagePath;
  final String? darkBackgroundImagePath;

  const BgScaffold({
    super.key,
    this.body,
    this.appBar,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.bottomNavigationBar,
    this.drawer,
    this.endDrawer,
    this.lightBackgroundImagePath,
    this.darkBackgroundImagePath,
  }) : assert(
          lightBackgroundImagePath != null || darkBackgroundImagePath != null,
          'At least one background image path must be provided',
        );

  @override
  Widget build(BuildContext context) {
    cacheImages(context, darkBackgroundImagePath, lightBackgroundImagePath);
    ThemeData theme = Theme.of(context);
    
    final isDark = theme.brightness == Brightness.dark;
    final imagePath = isDark
        ? (darkBackgroundImagePath ?? lightBackgroundImagePath!)
        : (lightBackgroundImagePath ?? darkBackgroundImagePath!);
    final loadingColor = Theme.of(context).colorScheme.surface;

    return Scaffold(
      backgroundColor: Colors.transparent,
      extendBodyBehindAppBar: true,
      appBar: appBar,
      drawer: drawer,
      endDrawer: endDrawer,
      floatingActionButton: floatingActionButton,
      floatingActionButtonLocation: floatingActionButtonLocation,
      bottomNavigationBar: bottomNavigationBar,
      body: Stack(
        children: [
          // Background image container with loading color
          Container(
            width: double.infinity,
            height: double.infinity,
            color: loadingColor,
            child: Image.asset(
              imagePath,
              fit: BoxFit.cover,
              cacheWidth: null,
              cacheHeight: null,
              frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
                if (wasSynchronouslyLoaded || frame != null) {
                  return child;
                }
                return Container(color: loadingColor);
              },
              errorBuilder: (context, error, stackTrace) {
                return Container(color: loadingColor);
              },
            ),
          ),
          // Actual body content
          if (body != null) body!,
        ],
      ),
    );
  }

  void cacheImages(
    BuildContext context,
    String? darkBackgroundImagePath,
    String? lightBackgroundImagePath,
  ) {
    if (MemorySettingsService().bgImagesCached){
      return;
    }
    if (darkBackgroundImagePath != null){
      precacheImage(AssetImage(darkBackgroundImagePath), context);
    }
    if (lightBackgroundImagePath != null){
      precacheImage(AssetImage(lightBackgroundImagePath), context);
    }
    MemorySettingsService().bgImagesCached = true;
  }
}

请注意 extendBodyBehindAppBar 这个属性。我以前不知道它有这个功能。

下面是我们如何使用 BgScaffold 的方法:

dart 复制代码
   return BgScaffold(
      darkBackgroundImagePath: 'assets/images/background/black_mramor.webp',
      lightBackgroundImagePath: 'assets/images/background/light_pink_flower.webp',
      appBar: AppBar(
        title: const Text('Registration'),
        centerTitle: true,
        backgroundColor: Colors.transparent,
      ),
      body: RegistrationPage(),
    );

与普通的 Scaffold 唯一的区别是,我们提供了背景图片的路径

frameBuilder 是一个回调函数 ,每当 Flutter 解码图像的一帧 时,它就会被调用。它主要用于两个目的:显示加载状态处理动画

以下是每个参数的含义:

dart 复制代码
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {  
// context - 标准的 BuildContext 
// child - 正在被解码的实际的 Image 组件 
// frame - 我们正在处理的是第几帧(0, 1, 2...),如果尚未加载,则为 null 
// wasSynchronouslyLoaded - 如果图像是从缓存加载的,则为 true;如果正在从磁盘/网络加载,则为 false
}

在我们的代码中:

dart 复制代码
if (wasSynchronouslyLoaded || frame != null) {
  return child;
}
return Container(color: loadingColor);

这段代码的意思是:"如果图片已经在缓存中 (即 wasSynchronouslyLoaded 为真),或者 我们至少解码了一帧 (即 frame != null ),就显示图片 。否则,就显示加载颜色。"

关键在于,对于已经预缓存asset 图片 来说,wasSynchronouslyLoaded 几乎总是 true ,所以加载颜色很少会显示 。这其实就是我们想要的效果------瞬间显示

我们在每一次构建时 都调用 cacheImages 方法,这效率上有点低 ,但(除了第一次之外)我们所做的只是检查 MemorySettingsService().bgImagesCached 这个变量 。我选择这种方式是为了让 BgScaffold 保持自包含(self-contained)。

或者 ,我们可以在应用启动时就缓存图片:

dart 复制代码
return MaterialApp(
      home: Builder(
        builder: (context) {
          cacheImages(context, darkBackgroundImagePath, 
                               lightBackgroundImagePath);
          
          return RegistrationScreen();
        },
      ),

并且,这样可能 就能摆脱 使用 MemorySettingsService().bgImagesCached 这个变量了。因为 MaterialApp 组件很少会被重建

无论是采用哪种方法,实际的图片加载和缓存都只会发生一次 ,所以两者之间并没有太大的区别

这就是我今天想分享的所有内容了。

感谢您的阅读!

相关推荐
IT_陈寒43 分钟前
Vue的响应式让我熬到凌晨三点,原来漏了这个小细节
前端·人工智能·后端
Blanche15001 小时前
利用 RAG 为答疑机器人扩展知识范围
前端
天若有情6731 小时前
【纯前端小工具】公历生日转农历,批量查询每年农历生日对应的公历日期(GitHub Pages在线直接用)
前端·javascript·github pages·农历转换·lunisolar·网页小工具
SoonITer1 小时前
怎样构建一个 Agent-friendly 的网站
前端·agent
颜进强1 小时前
01 · NestJS 是什么:用途、解决什么问题、与热门框架对比
前端·后端·ai编程
wendZzoo1 小时前
前端工程师的 3D 第一课:一个模型如何进入网页
前端
颜进强2 小时前
04 · NestJS 依赖注入:你在 `@Module` 写的 providers,和构造参数里那个类型,是怎么"对上"的?
前端·后端·ai编程
肆仲冬2 小时前
不用框架,用 TypeScript 从零搭一个 Agent 框架
前端
林语琛2 小时前
我写的 switch…break 被 Babel 偷偷吞了
前端·javascript·babel
烈风逍遥2 小时前
第六篇:RAG 知识库构建与检索全链路
前端·人工智能·后端