Flutter 框架跨平台鸿蒙开发 —— Image Widget 占位符技术

示例代码


dart 复制代码
import 'package:flutter/material.dart';

void main() => runApp(const ImagePlaceholderApp());

class ImagePlaceholderApp extends StatelessWidget {
  const ImagePlaceholderApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(title: const Text('Image Widget 占位符技术')),
        body: const SingleChildScrollView(
          padding: EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ColorPlaceholderExample(),
              SizedBox(height: 20),
              WidgetPlaceholderExample(),
              SizedBox(height: 20),
              GradientPlaceholderExample(),
              SizedBox(height: 20),
              SkeletonPlaceholderExample(),
            ],
          ),
        ),
      ),
    );
  }
}

class ColorPlaceholderExample extends StatelessWidget {
  const ColorPlaceholderExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: Text(
              '示例1:颜色占位符',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
          ),
          Center(
            child: Image.network(
              'https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=400',
              width: 300,
              height: 200,
              fit: BoxFit.cover,
              loadingBuilder: (context, child, loadingProgress) {
                if (loadingProgress == null) return child;
                return Container(
                  width: 300,
                  height: 200,
                  color: Colors.grey[200],
                );
              },
            ),
          ),
          const SizedBox(height: 12),
        ],
      ),
    );
  }
}

class WidgetPlaceholderExample extends StatelessWidget {
  const WidgetPlaceholderExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: Text(
              '示例2:Widget 占位符',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
          ),
          Center(
            child: Image.network(
              'https://images.unsplash.com/photo-1516961642265-531546e84af2?w=400',
              width: 300,
              height: 200,
              fit: BoxFit.cover,
              loadingBuilder: (context, child, loadingProgress) {
                if (loadingProgress == null) return child;
                return Container(
                  width: 300,
                  height: 200,
                  color: Colors.grey[100],
                  child: const Center(
                    child: Icon(Icons.image, size: 64, color: Colors.grey),
                  ),
                );
              },
            ),
          ),
          const SizedBox(height: 12),
        ],
      ),
    );
  }
}

class GradientPlaceholderExample extends StatelessWidget {
  const GradientPlaceholderExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: Text(
              '示例3:渐变色占位符',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
          ),
          Center(
            child: Image.network(
              'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400',
              width: 300,
              height: 200,
              fit: BoxFit.cover,
              loadingBuilder: (context, child, loadingProgress) {
                if (loadingProgress == null) return child;
                return Container(
                  width: 300,
                  height: 200,
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [Colors.blue[100]!, Colors.purple[100]!],
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                    ),
                  ),
                  child: const Center(
                    child: CircularProgressIndicator(),
                  ),
                );
              },
            ),
          ),
          const SizedBox(height: 12),
        ],
      ),
    );
  }
}

class SkeletonPlaceholderExample extends StatelessWidget {
  const SkeletonPlaceholderExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: Text(
              '示例4:骨架屏占位符',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
          ),
          Center(
            child: Image.network(
              'https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=400',
              width: 300,
              height: 200,
              fit: BoxFit.cover,
              loadingBuilder: (context, child, loadingProgress) {
                if (loadingProgress == null) return child;
                return const SkeletonLoader();
              },
            ),
          ),
          const SizedBox(height: 12),
        ],
      ),
    );
  }
}

class SkeletonLoader extends StatelessWidget {
  const SkeletonLoader({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 300,
      height: 200,
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [
            Colors.grey[300]!,
            Colors.grey[200]!,
            Colors.grey[300]!,
          ],
          begin: Alignment(-1, -1),
          end: Alignment(1, 1),
        ),
      ),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 1500),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [
              Colors.grey[300]!.withOpacity(0.5),
              Colors.grey[200]!.withOpacity(0.8),
              Colors.grey[300]!.withOpacity(0.5),
            ],
          ),
        ),
      ),
    );
  }
}

🚀 快速运行

示例应用

本篇文章的示例项目:flutter_examples/04_image_state_demo

运行步骤

bash 复制代码
# 进入示例项目目录
cd flutter_examples/04_image_state_demo

# 运行应用(鸿蒙虚拟机)
flutter run -d 127.0.0.1:5555

# 或运行应用(其他设备)
flutter run

演示内容

运行应用后,您将看到本篇文章的示例:

  • 示例1:颜色占位符 - 使用颜色作为占位符
  • 示例2:Widget 占位符 - 使用自定义 Widget 作为占位符
  • 示例3:渐变色占位符 - 使用渐变色作为占位符
  • 示例4:骨架屏占位符 - 使用骨架屏作为占位符

一、 前言

在图片加载时,显示一个占位符可以提升用户体验,让用户知道这里即将显示图片。占位符可以是简单的颜色、自定义的 Widget,或者更复杂的骨架屏。

本篇文章讲解常见的占位符技术。


二、 颜色占位符

2.1 基本用法

最简单的占位符就是使用单色背景。

dart 复制代码
Image.network(
  'https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=400',
  width: double.infinity,
  height: 200,
  fit: BoxFit.cover,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;

    return Container(
      width: double.infinity,
      height: 200,
      color: Colors.grey[200],
    );
  },
)

2.2 选择合适的颜色

dart 复制代码
// 1. 使用灰色系(最常用)
color: Colors.grey[200]

// 2. 使用主题色
color: Colors.blue[100]

// 3. 使用半透明白色
color: Colors.white.withOpacity(0.5)

三、 Widget 占位符

3.1 使用图标作为占位符

dart 复制代码
Image.network(
  'https://images.unsplash.com/photo-1516961642265-531546e84af2?w=400',
  width: double.infinity,
  height: 200,
  fit: BoxFit.cover,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;

    return Container(
      width: double.infinity,
      height: 200,
      color: Colors.grey[100],
      child: const Center(
        child: Icon(Icons.image, size: 64, color: Colors.grey),
      ),
    );
  },
)

3.2 使用进度指示器

dart 复制代码
Image.network(
  url,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;

    return Container(
      width: double.infinity,
      height: 200,
      color: Colors.blue[50],
      child: const Center(
        child: CircularProgressIndicator(),
      ),
    );
  },
)

四、 渐变色占位符

4.1 使用 LinearGradient

dart 复制代码
Image.network(
  'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400',
  width: double.infinity,
  height: 200,
  fit: BoxFit.cover,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;

    return Container(
      width: double.infinity,
      height: 200,
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.blue[100]!, Colors.purple[100]!],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
      ),
      child: const Center(
        child: CircularProgressIndicator(),
      ),
    );
  },
)

4.2 渐变方向选择

dart 复制代码
// 1. 从左上到右下
gradient: LinearGradient(
  colors: [Colors.blue[100]!, Colors.purple[100]!],
  begin: Alignment.topLeft,
  end: Alignment.bottomRight,
)

// 2. 从左到右
gradient: LinearGradient(
  colors: [Colors.blue[100]!, Colors.purple[100]!],
  begin: Alignment.centerLeft,
  end: Alignment.centerRight,
)

// 3. 从上到下
gradient: LinearGradient(
  colors: [Colors.blue[100]!, Colors.purple[100]!],
  begin: Alignment.topCenter,
  end: Alignment.bottomCenter,
)

五、 骨架屏占位符

5.1 什么是骨架屏

骨架屏是一种在数据加载时显示的占位动画,模拟内容的大致结构,给用户一种"内容即将到来"的感觉。

5.2 简单的骨架屏实现

dart 复制代码
Image.network(
  'https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=400',
  width: double.infinity,
  height: 200,
  fit: BoxFit.cover,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;

    return _SkeletonLoader();
  },
)

5.3 骨架屏 Widget 实现

dart 复制代码
class _SkeletonLoader extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: 200,
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [
            Colors.grey[300]!,
            Colors.grey[200]!,
            Colors.grey[300]!,
          ],
          begin: Alignment(-1, -1),
          end: Alignment(1, 1),
        ),
      ),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 1500),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [
              Colors.grey[300]!.withOpacity(0.5),
              Colors.grey[200]!.withOpacity(0.8),
              Colors.grey[300]!.withOpacity(0.5),
            ],
          ),
        ),
      ),
    );
  }
}

5.4 使用第三方库

更专业的骨架屏可以使用第三方库,如 shimmer

yaml 复制代码
# pubspec.yaml
dependencies:
  shimmer: ^3.0.0
dart 复制代码
import 'package:shimmer/shimmer.dart';

Shimmer.fromColors(
  baseColor: Colors.grey[300]!,
  highlightColor: Colors.grey[100]!,
  child: Container(
    width: 200,
    height: 200,
    color: Colors.white,
  ),
)

六、 占位符选择指南

场景 推荐占位符 原因
列表图片 颜色占位 简单快速,不影响性能
用户头像 Widget占位 使用用户图标,更具个性化
头部横幅 渐变占位 视觉效果更好
详情页 骨架屏 体验最佳,接近最终效果
卡片图片 颜色+Widget 简洁又有提示作用

七、 总结

占位符技术是提升图片加载体验的重要手段:

  1. 颜色占位:最简单,性能最好
  2. Widget占位:更直观,可以显示图标或文字
  3. 渐变占位:视觉效果更好
  4. 骨架屏:最佳体验,模拟真实内容结构

记住,好的占位符应该简洁、快速、有提示性。根据具体场景选择合适的占位符技术。


参考资源


欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net

相关推荐
2501_9219308315 分钟前
基础入门 React Native 鸿蒙跨平台开发:react-native-button三方库适配
react native·react.js·harmonyos
一起养小猫8 小时前
Flutter for OpenHarmony 进阶:体育计分系统与数据持久化深度解析
flutter·harmonyos
ujainu9 小时前
Flutter + OpenHarmony 游戏开发进阶:主菜单架构与历史最高分持久化
flutter·游戏·架构·openharmony
铅笔侠_小龙虾10 小时前
Flutter Demo
开发语言·javascript·flutter
2501_9445255410 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 账户详情页面
android·java·开发语言·前端·javascript·flutter
2601_9498574310 小时前
Flutter for OpenHarmony Web开发助手App实战:快捷键参考
前端·flutter
mocoding11 小时前
flutter通信小能手pigeon三方库已完成鸿蒙化适配
flutter·华为·harmonyos
一起养小猫11 小时前
Flutter for OpenHarmony 实战:2048游戏完整开发指南
flutter·游戏·harmonyos
仓颉编程语言12 小时前
鸿蒙仓颉编程语言挑战赛一等奖作品:MeetAI-基于Cangjie的智能会后整理助手
华为·鸿蒙·仓颉编程语言
大雷神13 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地--第31篇:桌面小组件开发
华为·harmonyos