加载动画是应用中不可或缺的反馈机制,当数据加载或操作处理时,通过动画告诉用户"正在进行中,请稍候"。说实话,一个好的加载动画不仅能缓解用户等待的焦虑,还能提升应用的品质感。

咱们这次要实现多种加载动画,包括圆形进度指示器、全屏加载遮罩、按钮加载状态等。做这个功能的时候,我一直在想怎么让等待变得不那么无聊,最后决定用流畅的动画配合合理的提示,让加载过程也成为良好体验的一部分。
请添加图片描述

基础加载指示器组件

实现最常用的圆形加载指示器。

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

class LoadingIndicator extends StatelessWidget {
  final String? message;
  final double? size;
  final Color? color;
  
  const LoadingIndicator({
    super.key,
    this.message,
    this.size,

组件参数:message是加载提示文字,size是指示器大小,color是指示器颜色,都是可选参数。

可选设计:参数都用?标记为可选,这样组件更灵活,可以只显示指示器,也可以配合文字和自定义颜色。

StatelessWidget:加载指示器本身不需要管理状态,动画由CircularProgressIndicator内部处理,所以用StatelessWidget。

通用组件:这个组件可以在多个地方使用,数据加载、操作处理、页面跳转等场景。

    this.color,
  });
  
  
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        mainAxisSize: MainAxisSize.min,
        children: [
          SizedBox(
            width: size ?? 40.w,
            height: size ?? 40.w,

Center居中:用Center让加载指示器在屏幕中央,这是加载动画的标准位置。

Column布局:用Column垂直排列指示器和提示文字,mainAxisAlignment.center让内容垂直居中。

mainAxisSize:设为min,让Column只占用必要的高度,不会拉伸填满整个屏幕。

SizedBox限制:用SizedBox限制CircularProgressIndicator的大小,默认40.w,也可以通过size参数自定义。

空值合并:用??运算符提供默认值,如果size为null,使用40.w作为默认大小。

            child: CircularProgressIndicator(
              strokeWidth: 3.w,
              valueColor: color != null 
                ? AlwaysStoppedAnimation<Color>(color!) 
                : null,
            ),
          ),
          if (message != null) ...[
            SizedBox(height: 16.h),
            Text(
              message!,
              style: TextStyle(
                fontSize: 14.sp,
                color: Colors.grey[600],
              ),
              textAlign: TextAlign.center,
            ),
          ],
        ],
      ),
    );
  }
}

CircularProgressIndicator:Flutter提供的圆形进度指示器,自动旋转动画,不需要手动控制。

strokeWidth:设置进度条的粗细为3.w,不要太粗也不要太细,视觉上刚好。

valueColor:如果传入了color参数,使用AlwaysStoppedAnimation设置颜色,否则使用默认颜色。

条件渲染:用if判断message是否为null,如果有提示文字就显示,没有就不显示。

文字样式:用14.sp的浅灰色显示提示文字,比如"加载中…"、“正在保存…”。字号小,颜色淡,是辅助信息。

文字居中:textAlign设为center,让文字居中对齐,与整体居中的布局保持一致。

全屏加载遮罩

实现覆盖整个屏幕的加载遮罩。

class LoadingOverlay extends StatelessWidget {
  final bool isLoading;
  final Widget child;
  final String? message;
  
  const LoadingOverlay({
    super.key,
    required this.isLoading,
    required this.child,
    this.message,
  });
  
  
  Widget build(BuildContext context) {
    return Stack(
      children: [
        child,

Stack布局:用Stack把加载遮罩叠加在内容上方,遮罩在最上层,内容在下层。

isLoading控制:用bool变量控制是否显示加载遮罩,true显示,false隐藏。

child参数:child是被遮罩的内容,通常是整个页面或某个区域。

message参数:可选的加载提示文字,显示在加载指示器下方。

        if (isLoading)
          Container(
            color: Colors.black54,
            child: Center(
              child: Card(
                child: Padding(
                  padding: EdgeInsets.all(24.w),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const CircularProgressIndicator(),
                      if (message != null) ...[

条件显示:用if判断isLoading,只有为true时才显示遮罩,避免不必要的渲染。

半透明背景:Container设置Colors.black54(半透明黑色),让用户知道界面被锁定,不能操作。

Card容器:用Card包裹加载指示器和文字,形成一个白色的卡片,与半透明背景形成对比。

Padding内边距:设置24.w的内边距,让加载指示器和文字不会紧贴Card边缘。

Column布局:mainAxisSize设为min,让Column只占用必要的高度,形成紧凑的加载提示框。

                        SizedBox(height: 16.h),
                        Text(
                          message!,
                          style: TextStyle(fontSize: 14.sp),
                        ),
                      ],
                    ],
                  ),
                ),
              ),
            ),
          ),
      ],
    );
  }
}

间距控制:指示器和文字之间用16.h的SizedBox分隔,形成清晰的视觉分组。

文字样式:用14.sp的默认颜色显示提示文字,在白色Card上清晰可见。

禁止交互:半透明背景会阻止用户点击下层内容,实现界面锁定效果。

使用示例:用LoadingOverlay包裹页面内容,通过isLoading控制显示隐藏,非常方便。

按钮加载状态

实现按钮的加载状态。

class LoadingButton extends StatelessWidget {
  final bool isLoading;
  final String text;
  final VoidCallback? onPressed;
  final Color? color;
  
  const LoadingButton({
    super.key,
    required this.isLoading,
    required this.text,
    this.onPressed,
    this.color,
  });
  
  
  Widget build(BuildContext context) {
    return ElevatedButton(

isLoading控制:用bool变量控制按钮是否处于加载状态,true显示加载指示器,false显示文字。

text参数:按钮的文字,比如"保存"、“提交”、"登录"等。

onPressed回调:按钮点击回调,加载时会被禁用,避免重复提交。

color参数:可选的按钮颜色,可以自定义按钮的主题色。

      onPressed: isLoading ? null : onPressed,
      style: color != null 
        ? ElevatedButton.styleFrom(backgroundColor: color)
        : null,
      child: isLoading
        ? SizedBox(
            width: 20.w,
            height: 20.w,
            child: const CircularProgressIndicator(
              strokeWidth: 2,
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
            ),
          )

禁用按钮:isLoading为true时,onPressed设为null,按钮自动禁用,变灰且不可点击。

自定义颜色:如果传入了color,使用styleFrom设置backgroundColor,否则使用默认颜色。

加载指示器:isLoading为true时,child显示CircularProgressIndicator,大小20x20,白色。

strokeWidth:设为2,比默认的细一些,因为按钮空间有限,太粗会显得拥挤。

白色指示器:valueColor设为白色,因为按钮背景通常是深色,白色指示器更清晰。

    );
  }
}

// 使用示例
LoadingButton(
  isLoading: controller.isLoading.value,
  text: '保存',
  onPressed: () => controller.save(),
  color: Colors.blue,
)

文字显示:isLoading为false时,child显示Text(text),正常的按钮文字。

状态切换:通过改变isLoading的值,按钮在加载和正常状态之间切换,UI自动更新。

使用简单:只需要传入isLoading、text和onPressed,就能实现完整的加载按钮功能。

响应式状态:配合GetX的Rx类型,isLoading.value变化时,按钮自动重建,无需手动setState。

列表加载状态

实现列表数据加载的状态管理。

enum LoadingState {
  idle,      // 空闲状态
  loading,   // 加载中
  success,   // 加载成功
  error,     // 加载失败
}

class ListLoadingWidget extends StatelessWidget {
  final LoadingState state;
  final String? errorMessage;
  final VoidCallback? onRetry;
  
  const ListLoadingWidget({
    super.key,
    required this.state,

枚举状态:用enum定义四种加载状态,idle、loading、success、error,清晰明确。

state参数:当前的加载状态,根据状态显示不同的UI。

errorMessage:加载失败时的错误信息,显示给用户。

onRetry回调:加载失败时的重试回调,让用户可以重新加载。

    this.errorMessage,
    this.onRetry,
  });
  
  
  Widget build(BuildContext context) {
    switch (state) {
      case LoadingState.loading:
        return const Center(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: CircularProgressIndicator(),
          ),
        );

switch语句:根据state的值,返回不同的Widget,代码清晰易维护。

loading状态:显示CircularProgressIndicator,居中显示,上下左右各16像素内边距。

简洁设计:loading状态只显示指示器,不显示文字,因为用户知道正在加载。

      case LoadingState.error:
        return Center(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.error_outline, size: 48, color: Colors.red),
                const SizedBox(height: 16),
                Text(
                  errorMessage ?? '加载失败',
                  style: const TextStyle(fontSize: 14),
                  textAlign: TextAlign.center,
                ),
                if (onRetry != null) ...[
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: onRetry,
                    child: const Text('重试'),
                  ),
                ],
              ],
            ),
          ),
        );

error状态:显示错误图标、错误信息和重试按钮,让用户知道出错了,并提供解决方案。

错误图标:用error_outline图标,红色,48像素大小,醒目地提示用户出错了。

错误信息:显示errorMessage,如果为null,显示默认的"加载失败"。

重试按钮:如果提供了onRetry回调,显示重试按钮,让用户可以重新加载数据。

友好提示:不仅告诉用户出错了,还提供了重试的方法,用户体验更好。

      case LoadingState.idle:
      case LoadingState.success:
        return const SizedBox.shrink();
    }
  }
}

idle和success:这两种状态不需要显示任何内容,返回SizedBox.shrink(),占用0空间。

SizedBox.shrink:创建一个0x0大小的Widget,比Container()更高效,专门用于不显示内容的场景。

状态完整:四种状态都有对应的UI,覆盖了列表加载的所有场景。

下拉刷新加载

实现下拉刷新功能。

RefreshIndicator(
  onRefresh: () async {
    await controller.refreshData();
  },
  child: ListView.builder(
    itemCount: controller.items.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(controller.items[index].title),
      );
    },
  ),
)

RefreshIndicator:Flutter提供的下拉刷新组件,用户下拉时显示加载指示器。

onRefresh回调:返回Future,执行异步加载操作,完成后RefreshIndicator自动隐藏。

async/await:用async/await处理异步操作,代码清晰易读。

ListView包裹:RefreshIndicator必须包裹可滚动的Widget,通常是ListView或GridView。

自动隐藏:onRefresh完成后,RefreshIndicator自动隐藏,不需要手动控制。

上拉加载更多

实现列表滚动到底部时加载更多数据。

class LoadMoreListView extends StatefulWidget {
  final List items;
  final Future<void> Function() onLoadMore;
  final Widget Function(BuildContext, int) itemBuilder;
  
  const LoadMoreListView({
    super.key,
    required this.items,
    required this.onLoadMore,
    required this.itemBuilder,
  });
  
  
  State<LoadMoreListView> createState() => _LoadMoreListViewState();
}

class _LoadMoreListViewState extends State<LoadMoreListView> {
  final ScrollController _scrollController = ScrollController();
  bool _isLoadingMore = false;

StatefulWidget:需要管理加载状态和滚动监听,所以用StatefulWidget。

items参数:列表数据,从外部传入。

onLoadMore回调:加载更多数据的回调,返回Future。

itemBuilder:列表项的构建函数,与ListView.builder的itemBuilder相同。

ScrollController:监听滚动事件,判断是否滚动到底部。

_isLoadingMore:标记是否正在加载更多,避免重复加载。

  
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }
  
  void _onScroll() {
    if (_scrollController.position.pixels >= 
        _scrollController.position.maxScrollExtent - 200) {
      if (!_isLoadingMore) {
        _loadMore();
      }
    }
  }
  
  Future<void> _loadMore() async {
    setState(() => _isLoadingMore = true);
    await widget.onLoadMore();
    setState(() => _isLoadingMore = false);
  }

添加监听:initState中给ScrollController添加滚动监听。

滚动判断:当滚动位置距离底部小于200像素时,触发加载更多。

防重复:用_isLoadingMore标记,避免在加载过程中重复触发。

状态更新:加载前后调用setState更新_isLoadingMore,驱动UI更新。

异步加载:用async/await执行onLoadMore回调,等待加载完成。

  
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
  
  
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: widget.items.length + 1,
      itemBuilder: (context, index) {
        if (index < widget.items.length) {
          return widget.itemBuilder(context, index);
        } else {
          return _isLoadingMore
            ? const Center(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: CircularProgressIndicator(),
                ),
              )
            : const SizedBox.shrink();
        }
      },
    );
  }
}

资源释放:dispose中释放ScrollController,避免内存泄漏。

itemCount:列表项数量加1,最后一项用于显示加载指示器。

条件渲染:如果index小于items.length,显示正常的列表项,否则显示加载指示器。

加载指示器:_isLoadingMore为true时显示CircularProgressIndicator,false时显示SizedBox.shrink()。

完整功能:实现了滚动监听、自动加载、状态管理、UI更新的完整流程。

骨架屏加载

实现骨架屏加载效果。

class SkeletonLoader extends StatelessWidget {
  final int itemCount;
  
  const SkeletonLoader({
    super.key,
    this.itemCount = 5,
  });
  
  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: itemCount,
      itemBuilder: (context, index) {
        return Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              _buildSkeleton(60, 60, isCircle: true),
              const SizedBox(width: 16),

itemCount参数:骨架屏显示的项数,默认5个,可以自定义。

ListView.builder:用ListView.builder创建多个骨架项,模拟真实列表的结构。

Padding内边距:每个骨架项设置16像素内边距,与真实列表项保持一致。

Row布局:用Row横向排列圆形头像和文字区域,模拟列表项的布局。

_buildSkeleton:自定义方法,创建骨架占位块,isCircle参数控制是否是圆形。

              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _buildSkeleton(double.infinity, 16),
                    const SizedBox(height: 8),
                    _buildSkeleton(200, 14),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
  
  Widget _buildSkeleton(double width, double height, {bool isCircle = false}) {
    return Container(
      width: width,
      height: height,
      decoration: BoxDecoration(
        color: Colors.grey[300],
        borderRadius: isCircle ? null : BorderRadius.circular(4),
        shape: isCircle ? BoxShape.circle : BoxShape.rectangle,
      ),
    );
  }
}

Expanded扩展:文字区域用Expanded占据剩余空间,与真实布局一致。

Column布局:用Column垂直排列标题和副标题的骨架块。

不同尺寸:标题用double.infinity宽度,高度16;副标题用200宽度,高度14,模拟真实文字的大小。

_buildSkeleton方法:创建灰色的占位块,width和height控制大小,isCircle控制形状。

圆形头像:isCircle为true时,shape设为BoxShape.circle,创建圆形骨架。

圆角矩形:isCircle为false时,用BorderRadius.circular(4)创建圆角矩形。

颜色选择:用Colors.grey[300],浅灰色,与背景色对比不要太强,避免刺眼。

总结

加载动画是应用中重要的反馈机制,通过流畅的动画和清晰的提示,让用户知道操作正在进行中。从简单的圆形指示器到复杂的骨架屏,从按钮加载到全屏遮罩,不同场景需要不同的加载方式。

说实话,做加载动画让我对用户体验有了更深的理解。加载不可避免,但可以让等待变得不那么无聊。动画要流畅,不要卡顿,否则会让用户更焦虑。提示要清晰,告诉用户在加载什么,还需要多久。状态要管理好,避免加载状态混乱。交互要禁用,加载时不能让用户重复操作。

如果你也在做加载功能,建议重点关注动画的流畅性和提示的清晰性。动画要简洁,不要过度复杂,避免影响性能。提示要有用,告诉用户具体信息,而不是只说"加载中"。状态要完整,idle、loading、success、error都要考虑。错误要处理,提供重试按钮,给用户解决问题的机会。骨架屏要逼真,模拟真实内容的布局,让用户有心理预期。

欢迎加入OpenHarmony跨平台开发社区交流:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐