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

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

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

感谢您的阅读!

相关推荐
百锦再6 小时前
Reactive编程入门:Project Reactor 深度指南
前端·javascript·python·react.js·django·前端框架·reactjs
莲华君6 小时前
React快速上手:从零到项目实战
前端·reactjs教程
百锦再6 小时前
React编程高级主题:测试代码
android·前端·javascript·react.js·前端框架·reactjs
易安说AI6 小时前
Ralph Loop 让Claude无止尽干活的牛马...
前端·后端
失忆爆表症8 小时前
05_UI 组件库集成指南:Shadcn/ui + Tailwind CSS v4
前端·css·ui
小迷糊的学习记录8 小时前
Vuex 与 pinia
前端·javascript·vue.js
发现一只大呆瓜8 小时前
前端性能优化:图片懒加载的三种手写方案
前端·javascript·面试
不爱吃糖的程序媛8 小时前
Flutter 与 OpenHarmony 通信:Flutter Channel 使用指南
前端·javascript·flutter
利刃大大8 小时前
【Vue】Element-Plus快速入门 && Form && Card && Table && Tree && Dialog && Menu
前端·javascript·vue.js·element-plus
NEXT069 小时前
AI 应用工程化实战:使用 LangChain.js 编排 DeepSeek 复杂工作流
前端·javascript·langchain