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 组件很少会被重建

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

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

感谢您的阅读!

相关推荐
BINGCHN1 分钟前
NSSCTF每日一练 SWPUCTF2021 include--web
android·前端·android studio
Z***u65934 分钟前
前端性能测试实践
前端
xhxxx37 分钟前
prototype 是遗产,proto 是族谱:一文吃透 JS 原型链
前端·javascript
倾墨38 分钟前
Bytebot源码学习
前端
用户938169125536042 分钟前
VUE3项目--集成Sass
前端
S***H2831 小时前
Vue语音识别案例
前端·vue.js·语音识别
啦啦9118861 小时前
【版本更新】Edge 浏览器 v142.0.3595.94 绿色增强版+官方安装包
前端·edge
蚂蚁集团数据体验技术2 小时前
一个可以补充 Mermaid 的可视化组件库 Infographic
前端·javascript·llm
LQW_home2 小时前
前端展示 接受springboot Flux数据demo
前端·css·css3
q***d1732 小时前
前端增强现实案例
前端·ar