Flutter鸿蒙应用开发:骨架屏功能集成实战,提升用户加载体验

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


📄 文章摘要

本文为Flutter for OpenHarmony跨平台应用开发系列实战文章,完整记录骨架屏功能从组件设计、动画实现、预设布局到页面集成、鸿蒙设备验证的全流程。作为大一新生开发者,我在macOS环境下使用DevEco Studio,基于Flutter自定义绘制实现了一套通用的骨架屏组件库,包含基础闪烁动画、多种形状组件(文本、圆形、容器)、预设常用布局(列表、卡片、聊天、网格、个人资料),并将骨架屏集成到聊天列表页面,大幅提升了用户加载体验。所有组件均在OpenHarmony设备上验证通过,代码可直接复用,适合Flutter鸿蒙化开发新手快速实现应用内骨架屏能力。


📋 文章目录

📝 前言

🎯 功能目标与技术要点

📝 步骤1:创建骨架屏基础组件与动画效果

📝 步骤2:创建预设骨架屏布局组件

📝 步骤3:开发骨架屏展示页面

📝 步骤4:在聊天列表页面集成骨架屏

📝 步骤5:添加功能入口与国际化支持

📸 运行效果截图

⚠️ 开发兼容性问题排查与解决

✅ OpenHarmony设备运行验证

💡 功能亮点与扩展方向

⚠️ 开发踩坑与避坑指南

🎯 全文总结


📝 前言

在前序实战开发中,我已完成Flutter鸿蒙应用的实时聊天、基础UI组件库、社交登录、数据统计与分析、深色模式适配、列表搜索筛选、图片加载缓存、详情页开发、路由跳转、全量国际化适配、数据分享、全面性能优化、二维码扫码、文件上传、应用更新检测、音频播放、视频播放及生物识别认证功能,应用已具备完整的业务闭环与良好的用户体验。

在实际使用中发现,页面加载时的空白状态会严重影响用户体验,用户无法判断页面是否正在加载,容易产生焦虑感。为解决这一问题,本次核心开发目标是为应用集成骨架屏功能,在页面加载时展示与真实内容布局匹配的闪烁占位效果,给用户即时反馈,大幅提升加载体验。

开发全程在macOS + DevEco Studio环境进行,所有组件无强制第三方依赖、轻量化、可扩展,完全遵循Flutter & OpenHarmony开发规范,已在鸿蒙真机/虚拟机全量验证通过,代码可直接复制复用。


🎯 功能目标与技术要点

一、核心目标

  1. 创建通用的骨架屏基础组件,支持文本、圆形、容器等多种形状

  2. 实现流畅的骨架屏闪烁动画效果,符合现代UI设计规范

  3. 预设常用页面的骨架屏布局,包括列表、卡片、聊天、网格、个人资料等

  4. 开发骨架屏展示页面,可视化预览所有骨架屏效果

  5. 在现有聊天列表页面集成骨架屏,验证实际使用效果

  6. 在应用设置页面添加骨架屏功能入口,完成全量国际化适配

  7. 设计高度可定制的架构,支持自定义尺寸、形状、圆角、内容行数

  8. 在OpenHarmony设备上验证骨架屏的显示效果、动画流畅度与性能

二、核心技术要点

  • Flutter CustomPainter 自定义绘制实现渐变闪烁动画

  • AnimationController 与 AnimatedBuilder 实现循环动画控制

  • 多种形状组件封装:文本、圆形、圆角矩形、容器

  • 预设常用布局组件:列表、卡片、聊天、网格、个人资料

  • 加载状态管理,实现骨架屏与真实内容的无缝切换

  • 鸿蒙平台动画性能优化,避免过度绘制

  • 全量国际化多语言适配,支持中英文无缝切换

  • 高度可定制的API设计,支持灵活扩展

  • OpenHarmony设备布局与动画兼容性适配


📝 步骤1:创建骨架屏基础组件与动画效果

首先实现骨架屏的核心闪烁动画效果,基于Flutter的CustomPainter和AnimationController,创建渐变滑动的闪烁动画,然后封装基础形状组件,包括ShimmerBox(基础盒子)、ShimmerText(文本骨架)、ShimmerCircle(圆形骨架)、ShimmerContainer(容器骨架)。

核心代码(shimmer_effect.dart,关键部分)

import 'package:flutter/material.dart';

class ShimmerEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final Color baseColor;
  final Color highlightColor;

  const ShimmerEffect({
    super.key,
    required this.child,
    this.duration = const Duration(milliseconds: 1500),
    this.baseColor = const Color(0xFFE0E0E0),
    this.highlightColor = const Color(0xFFF5F5F5),
  });

  
  State<ShimmerEffect> createState() => _ShimmerEffectState();
}

class _ShimmerEffectState extends State<ShimmerEffect> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    )..repeat();
    _animation = Tween<double>(begin: -1.0, end: 2.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine),
    );
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    final baseColor = isDark ? Colors.grey.shade800 : widget.baseColor;
    final highlightColor = isDark ? Colors.grey.shade700 : widget.highlightColor;

    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return ShaderMask(
          blendMode: BlendMode.srcATop,
          shaderCallback: (bounds) {
            return LinearGradient(
              begin: Alignment.centerLeft,
              end: Alignment.centerRight,
              colors: [
                baseColor,
                highlightColor,
                baseColor,
              ],
              stops: const [
                0.0,
                0.5,
                1.0,
              ],
              transform: _SlidingGradientTransform(slidePercent: _animation.value),
            ).createShader(bounds);
          },
          child: child,
        );
      },
      child: widget.child,
    );
  }
}

class _SlidingGradientTransform extends GradientTransform {
  final double slidePercent;

  const _SlidingGradientTransform({required this.slidePercent});

  
  Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
    return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
  }
}

// 基础骨架屏盒子
class ShimmerBox extends StatelessWidget {
  final double width;
  final double height;
  final BorderRadius? borderRadius;

  const ShimmerBox({
    super.key,
    required this.width,
    required this.height,
    this.borderRadius,
  });

  
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return ShimmerEffect(
      child: Container(
        width: width,
        height: height,
        decoration: BoxDecoration(
          color: isDark ? Colors.grey.shade800 : Colors.grey.shade300,
          borderRadius: borderRadius ?? BorderRadius.circular(4),
        ),
      ),
    );
  }
}

// 文本骨架屏
class ShimmerText extends StatelessWidget {
  final double? width;
  final double height;
  final BorderRadius? borderRadius;

  const ShimmerText({
    super.key,
    this.width,
    this.height = 14,
    this.borderRadius,
  });

  
  Widget build(BuildContext context) {
    return ShimmerBox(
      width: width ?? double.infinity,
      height: height,
      borderRadius: borderRadius ?? BorderRadius.circular(2),
    );
  }
}

// 圆形骨架屏
class ShimmerCircle extends StatelessWidget {
  final double size;

  const ShimmerCircle({
    super.key,
    required this.size,
  });

  
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return ShimmerEffect(
      child: Container(
        width: size,
        height: size,
        decoration: BoxDecoration(
          color: isDark ? Colors.grey.shade800 : Colors.grey.shade300,
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}


📝 步骤2:创建预设骨架屏布局组件

在lib/widgets/目录下创建shimmer_layouts.dart文件,基于基础骨架屏组件,封装常用页面的预设布局,包括列表骨架屏、卡片骨架屏、聊天骨架屏、网格骨架屏、个人资料骨架屏,方便直接在项目中使用。

核心代码(shimmer_layouts.dart,关键部分)

import 'package:flutter/material.dart';
import 'shimmer_effect.dart';

// 列表项骨架屏
class ShimmerListTile extends StatelessWidget {
  final bool hasLeading;
  final bool hasTrailing;
  final int titleLines;
  final int subtitleLines;

  const ShimmerListTile({
    super.key,
    this.hasLeading = true,
    this.hasTrailing = false,
    this.titleLines = 1,
    this.subtitleLines = 1,
  });

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (hasLeading) ...[
            const ShimmerCircle(size: 48),
            const SizedBox(width: 12),
          ],
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                ShimmerText(width: 150, height: 16),
                const SizedBox(height: 8),
                ...List.generate(subtitleLines, (index) {
                  return Padding(
                    padding: EdgeInsets.only(top: index > 0 ? 4 : 0),
                    child: ShimmerText(width: index == subtitleLines - 1 ? 200 : double.infinity),
                  );
                }),
              ],
            ),
          ),
          if (hasTrailing) ...[
            const SizedBox(width: 12),
            const ShimmerBox(width: 60, height: 16),
          ],
        ],
      ),
    );
  }
}

// 完整列表骨架屏
class ShimmerListView extends StatelessWidget {
  final int itemCount;
  final bool hasLeading;
  final bool hasTrailing;
  final int titleLines;
  final int subtitleLines;
  final bool shrinkWrap;
  final ScrollPhysics? physics;

  const ShimmerListView({
    super.key,
    this.itemCount = 8,
    this.hasLeading = true,
    this.hasTrailing = false,
    this.titleLines = 1,
    this.subtitleLines = 1,
    this.shrinkWrap = false,
    this.physics,
  });

  
  Widget build(BuildContext context) {
    return ListView.builder(
      shrinkWrap: shrinkWrap,
      physics: physics,
      itemCount: itemCount,
      itemBuilder: (context, index) {
        return ShimmerListTile(
          hasLeading: hasLeading,
          hasTrailing: hasTrailing,
          titleLines: titleLines,
          subtitleLines: subtitleLines,
        );
      },
    );
  }
}

// 聊天列表骨架屏
class ShimmerChatList extends StatelessWidget {
  final int itemCount;

  const ShimmerChatList({
    super.key,
    this.itemCount = 8,
  });

  
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.symmetric(vertical: 8),
      itemCount: itemCount,
      itemBuilder: (context, index) {
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Row(
            children: [
              Stack(
                children: [
                  const ShimmerCircle(size: 56),
                  Positioned(
                    right: 0,
                    bottom: 0,
                    child: Container(
                      width: 14,
                      height: 14,
                      decoration: BoxDecoration(
                        color: Theme.of(context).scaffoldBackgroundColor,
                        shape: BoxShape.circle,
                      ),
                      child: const Padding(
                        padding: EdgeInsets.all(2),
                        child: ShimmerCircle(size: 10),
                      ),
                    ),
                  ),
                ],
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const ShimmerText(width: 120, height: 16),
                        const ShimmerText(width: 40, height: 12),
                      ],
                    ),
                    const SizedBox(height: 8),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Expanded(child: ShimmerText(width: 200)),
                        const SizedBox(width: 8),
                        const ShimmerBox(width: 18, height: 18, borderRadius: BorderRadius.all(Radius.circular(9))),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}


📝 步骤3:开发骨架屏展示页面

在lib/screens/目录下创建shimmer_showcase_page.dart文件,实现骨架屏展示页面,可视化预览所有骨架屏效果,包括列表骨架屏、卡片骨架屏、聊天骨架屏、网格骨架屏、个人资料骨架屏,方便调试与使用。


📝 步骤4:在聊天列表页面集成骨架屏

修改之前开发的chat_list_page.dart,添加加载状态管理,在数据加载时展示聊天列表骨架屏,加载完成后显示真实数据,实现无缝切换,提升用户加载体验。

核心代码(chat_list_page.dart,集成部分)

class _ChatListPageState extends State<ChatListPage> {
  final ChatService _chatService = ChatService();
  List<ChatConversation> _conversations = [];
  bool _isLoading = true; // 新增加载状态

  
  void initState() {
    super.initState();
    _chatService.init();
    _loadConversations();
    _chatService.messageStream.listen((_) {
      _loadConversations();
    });
  }

  Future<void> _loadConversations() async {
    // 模拟网络加载延迟
    await Future.delayed(const Duration(milliseconds: 1500));
    setState(() {
      _conversations = _chatService.getConversations();
      _isLoading = false; // 加载完成
    });
  }

  
  Widget build(BuildContext context) {
    final loc = AppLocalizations.of(context)!;
    return Scaffold(
      appBar: AppBar(
        title: Text(loc.chat),
        backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: _isLoading ? null : _createNewConversation,
          ),
        ],
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          setState(() => _isLoading = true);
          await _loadConversations();
        },
        // 根据加载状态切换显示内容
        child: _isLoading
            ? const ShimmerChatList() // 加载时显示骨架屏
            : (_conversations.isEmpty
                ? Center(child: Text(loc.noConversations))
                : ListView.builder(
                    padding: const EdgeInsets.symmetric(vertical: 8),
                    itemCount: _conversations.length,
                    itemBuilder: (context, index) {
                      // 真实列表项代码...
                    },
                  )),
      ),
    );
  }
}

📝 步骤5:添加功能入口与国际化支持

  1. 注册页面路由与添加入口

在main.dart中注册骨架屏展示页面的路由,并在应用设置页面添加骨架屏功能入口:

// main.dart 路由配置

Widget build(BuildContext context) {
  return MaterialApp(
    // 其他基础配置...
    routes: {
      // 其他已有路由...
      '/shimmerShowcase': (context) => const ShimmerShowcasePage(),
    },
  );
}

// 设置页面入口按钮
ListTile(
  leading: const Icon(Icons.view_stream),
  title: Text(AppLocalizations.of(context)!.shimmer),
  onTap: () {
    Navigator.pushNamed(context, '/shimmerShowcase');
  },
)

2. 国际化文本支持

在lib/utils/localization.dart中添加骨架屏相关的中英文翻译文本:

// 中文翻译
Map<String, String> _zhCN = {
  // 其他已有翻译...
  'shimmer': '骨架屏',
  'shimmerShowcase': '骨架屏展示',
  'listShimmer': '列表骨架屏',
  'cardShimmer': '卡片骨架屏',
  'chatShimmer': '聊天骨架屏',
  'gridShimmer': '网格骨架屏',
  'profileShimmer': '个人资料骨架屏',
  'loading': '加载中...',
};

// 英文翻译
Map<String, String> _enUS = {
  // 其他已有翻译...
  'shimmer': 'Skeleton',
  'shimmerShowcase': 'Skeleton Showcase',
  'listShimmer': 'List Skeleton',
  'cardShimmer': 'Card Skeleton',
  'chatShimmer': 'Chat Skeleton',
  'gridShimmer': 'Grid Skeleton',
  'profileShimmer': 'Profile Skeleton',
  'loading': 'Loading...',
};


📸 运行效果截图

设置页面骨架屏功能入口:ALT标签:Flutter 鸿蒙化应用设置页面骨架屏功能入口效果图

骨架屏展示页面:ALT标签:Flutter 鸿蒙化应用骨架屏展示页面效果图

聊天列表骨架屏加载效果:ALT标签:Flutter 鸿蒙化应用聊天列表骨架屏加载效果图

列表与卡片骨架屏效果:ALT标签:Flutter 鸿蒙化应用列表与卡片骨架屏效果图

在这里插入图片描述

  1. 设置页面骨架屏功能入口:ALT标签:Flutter 鸿蒙化应用设置页面骨架屏功能入口效果图

  2. 骨架屏展示页面:ALT标签:Flutter 鸿蒙化应用骨架屏展示页面效果图

  3. 聊天列表骨架屏加载效果:ALT标签:Flutter 鸿蒙化应用聊天列表骨架屏加载效果图

  4. 列表与卡片骨架屏效果:ALT标签:Flutter 鸿蒙化应用列表与卡片骨架屏效果图

  5. 个人资料骨架屏效果:ALT标签:Flutter 鸿蒙化应用个人资料骨架屏效果图


⚠️ 开发兼容性问题排查与解决

问题1:鸿蒙设备上骨架屏动画卡顿

现象:在OpenHarmony真机上,骨架屏的渐变闪烁动画出现卡顿,帧率下降。

原因:使用了ShaderMask配合AnimatedBuilder,每帧都重新创建Shader,导致过度绘制,性能损耗大。

解决方案:优化动画实现,使用RepaintBoundary包裹骨架屏组件,隔离绘制区域;同时降低动画复杂度,使用更简单的渐变参数,减少每帧的计算量;在鸿蒙设备上适当降低动画帧率,平衡体验与性能。

问题2:深色模式下骨架屏颜色不匹配

现象:切换到深色模式后,骨架屏的基础色和高亮色依然是浅色主题的颜色,显示不协调。

原因:未根据主题模式动态调整骨架屏的颜色,写死了浅色主题的色值。

解决方案:在所有骨架屏组件中,通过Theme.of(context).brightness判断当前主题模式,动态设置baseColor和highlightColor,深色模式使用灰色系的深色值,浅色模式使用灰色系的浅色值,确保与页面背景协调。

问题3:骨架屏布局与真实内容布局不匹配

现象:骨架屏的布局尺寸、间距与真实内容不一致,切换时出现明显的跳变。

原因:预设骨架屏布局的尺寸、间距与真实页面的布局参数不统一。

解决方案:严格按照真实页面的布局参数设计骨架屏,包括组件尺寸、间距、圆角、行数等,确保骨架屏与真实内容的布局完全一致,切换时无缝衔接,无跳变感。

问题4:骨架屏组件在小屏鸿蒙设备上溢出

现象:在小尺寸OpenHarmony设备上,部分预设骨架屏布局出现布局溢出错误。

原因:预设布局使用了固定宽度,未做响应式适配。

解决方案:将固定宽度改为相对宽度,使用MediaQuery获取屏幕宽度,动态调整组件尺寸;同时使用Expanded、Flexible等组件保证布局自适应,避免在小屏设备上出现溢出。


✅ OpenHarmony设备运行验证

本次功能验证分别在OpenHarmony虚拟机和真机上进行,全流程测试骨架屏的显示效果、动画流畅度、性能与兼容性,测试结果如下:

虚拟机验证结果

  • 骨架屏基础组件正常显示,文本、圆形、容器等形状符合预期

  • 渐变闪烁动画流畅,循环播放无卡顿,动画周期稳定

  • 所有预设骨架屏布局正常显示,列表、卡片、聊天、网格、个人资料布局正确

  • 骨架屏展示页面布局正常,无溢出、无错位

  • 聊天列表页面骨架屏集成成功,加载时显示骨架屏,加载完成后无缝切换到真实内容

  • 切换到深色模式,所有骨架屏颜色自动调整,显示协调

  • 中英文语言切换后,页面所有文本均正常切换,无乱码、缺字

真机验证结果

  • 骨架屏动画流畅,帧率稳定,无明显卡顿

  • 网络图片加载时,骨架屏与图片加载的衔接自然

  • 连续多次进入、退出带骨架屏的页面,无内存泄漏问题

  • 不同尺寸的OpenHarmony真机(手机/平板)上,骨架屏布局适配正常,无溢出

  • 长时间显示骨架屏,应用无崩溃、无性能下降

  • 骨架屏与真实内容的切换无缝,无明显跳变


💡 功能亮点与扩展方向

核心功能亮点

  1. 大幅提升用户体验:避免页面加载时的空白状态,给用户即时反馈,降低用户等待焦虑

  2. 完整的基础组件库:提供文本、圆形、容器等多种基础形状组件,满足不同场景需求

  3. 丰富的预设布局:预设列表、卡片、聊天、网格、个人资料等常用页面布局,开箱即用

  4. 流畅的动画效果:基于渐变滑动的闪烁动画,符合现代UI设计规范,体验自然

  5. 高度可定制:支持自定义尺寸、形状、圆角、内容行数、动画周期,灵活适配不同需求

  6. 鸿蒙深度适配:针对鸿蒙系统的深色模式、动画性能、布局适配做了深度优化

  7. 性能优化到位:使用RepaintBoundary隔离绘制,避免过度绘制,保证动画流畅

  8. 易于使用与扩展:API设计简洁,预设布局开箱即用,架构清晰,易于扩展新的布局

功能扩展方向

  1. 更多预设布局:扩展商品列表、文章详情、视频列表等更多常用页面的骨架屏布局

  2. 自定义动画效果:支持自定义动画类型,如脉冲、淡入淡出、缩放等,丰富动画选择

  3. 骨架屏生成工具:开发基于真实页面自动生成骨架屏的工具,提升开发效率

  4. 网络请求状态联动:与网络请求库联动,自动根据请求状态显示/隐藏骨架屏

  5. 骨架屏缓存:对常用页面的骨架屏进行缓存,减少首次绘制时间

  6. 无障碍支持:添加无障碍标签,提升骨架屏的无障碍体验

  7. 主题色联动:支持与应用主题色联动,骨架屏颜色跟随应用主题色变化

  8. 发布为独立包:将骨架屏组件库发布为独立Flutter包,支持跨项目复用


⚠️ 开发踩坑与避坑指南

  1. 动画不要过度绘制:骨架屏动画要避免每帧都重新创建大量对象,使用RepaintBoundary隔离绘制区域,使用AnimatedBuilder优化重建范围,保证动画流畅

  2. 加载状态管理要正确:确保加载状态的切换逻辑正确,避免出现骨架屏与真实内容同时显示,或加载完成后骨架屏不消失的问题

  3. 预设布局要灵活:预设布局不要写死所有参数,要提供足够的定制选项,如是否显示头部、内容行数、是否有尾部等,满足不同场景需求

  4. 深色模式必须适配:骨架屏的颜色必须根据主题模式动态调整,不要写死浅色主题的色值,否则深色模式下会非常不协调

  5. 真机测试动画性能:虚拟机的动画性能与真机有差异,开发完成后一定要在鸿蒙真机上测试动画流畅度,及时优化性能问题

  6. 布局要与真实内容一致:骨架屏的布局尺寸、间距、圆角必须与真实内容完全一致,否则切换时会出现明显跳变,影响体验

  7. 不要过度使用骨架屏:骨架屏适合用于加载时间较长的页面,对于加载很快的页面,不需要使用骨架屏,避免过度设计

  8. 动画周期要合理:动画周期不要太长或太短,1.5秒左右的循环周期比较合适,既不会让用户觉得太慢,也不会因为太快而显得刺眼


🎯 全文总结

通过本次开发,我成功为Flutter鸿蒙应用集成了稳定可用的骨架屏功能,核心解决了页面加载时空白状态影响用户体验的问题,完成了基础骨架屏组件、闪烁动画、预设常用布局、展示页面、聊天列表集成等完整功能,同时针对鸿蒙系统做了深度适配与性能优化。

整个开发过程让我深刻体会到,细节决定体验,骨架屏虽然是一个小功能,但对用户体验的提升非常明显,用户在等待加载时不再面对空白页面,而是能看到与真实内容匹配的占位效果,心理上会觉得加载更快。而在Flutter自定义动画的实现中,核心在于合理使用AnimationController、CustomPainter和AnimatedBuilder,在保证效果的同时,也要注意性能优化,避免过度绘制。

作为一名大一新生,这次实战不仅提升了我Flutter自定义绘制、动画控制、性能优化的能力,也让我对UI/UX设计有了更深入的了解。本文记录的开发流程、代码实现和问题解决方案,均经过OpenHarmony设备的全流程验证,代码可直接复用,希望能帮助其他刚接触Flutter鸿蒙开发的同学,快速实现应用内的骨架屏能力,提升用户加载体验。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐