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

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

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


关于性能(Performance)

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

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

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


看看例子(Examples)

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

ColorScheme (配色方案)是由 ChatGPT 生成 的。一个是为了 surface color (表面颜色)是 pink.shade100 ,第二个是为了 surface color 是 grey.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 组件很少会被重建。

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

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

感谢您的阅读!

相关推荐
w2sfot10 分钟前
AI时代,JS加密还有效吗?
开发语言·前端·javascript
风骏时光牛马15 分钟前
AI服务线上接口异常导致响应中断
前端
haluhalu.24 分钟前
Agent = LLM + 上下文 + 工具(含观察 / 动作空间、ReAct 循环)
前端·javascript·人工智能·python·react.js·机器学习·个人开发
子兮曰6 小时前
把 649 种文件格式塞进一个网页里:一个「文件不出浏览器」的预览器
前端·javascript·后端
sm_926787059 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
IMPYLH9 小时前
HTML 的 <video> 元素
前端·html
溪语流沙10 小时前
【每天一个CSS | Day19】流光文字与一笔写成的手写签名
前端·javascript·css
逐光者93310 小时前
STM32——SPI 屏 · Flash · 高速
java·前端·stm32
To_OC10 小时前
同样是让 AI 写全栈,为什么别人一句话就够了
前端·前端框架·next.js
CDwenhuohuo10 小时前
electron pc项目打包成桌面端
前端·javascript·electron