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

相关推荐
消失的旧时光-19431 小时前
Android 接入 Flutter(Add-to-App)最小闭环:10 分钟跑起第一个混合页面
android·flutter
Zender Han2 小时前
Flutter Android 启动页 & App 图标替换(不使用任何插件的完整实践)
android·flutter·ios
世人万千丶2 小时前
鸿蒙跨端框架Flutter学习day 2、常用UI组件-层叠布局 Stack & Positioned
学习·flutter·ui·实时互动·harmonyos·鸿蒙
酒醉的胡铁3 小时前
uniapp运行到鸿蒙证书配置
服务器·uni-app·harmonyos
五羟基己醛3 小时前
【HarmonyOS开发】鸿蒙ArkTS应用题目详解【完成】
华为
hefengbao4 小时前
【京墨文库】安卓版 v.16.1, 鸿蒙版 v1.2.1发布
华为·harmonyos
lili-felicity5 小时前
React Native for HarmonyOS (鸿蒙) 实战精讲:2D/3D 变换全场景
react native·3d·harmonyos
C雨后彩虹5 小时前
无向图染色
java·数据结构·算法·华为·面试
—Qeyser5 小时前
Flutter CustomScrollView 自定义滚动视图 - 完全指南
android·flutter·ios