import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; /// ============================================================ /// Image 图片组件演示页 /// /// 一、两种使用方式: /// 1. 直接运行本页面:runApp(const MaterialApp(home: ImageDemoPage())) /// 2. 复制某段核心代码到任意 Scaffold 的 body 中。 /// /// 二、加载图片来源的几种方式: /// CachedNetworkImage : 网络图片 + 本地缓存(生产推荐,二次加载秒出) /// Image.network : 网络图片(无缓存,每次重新下载,仅作对比) /// Image.asset : 本地资源图片(必须先注册 pubspec.yaml 的 assets) /// Image.file : 本地磁盘文件(需要 path_provider,本页不演示) /// Image.memory : 内存字节流(base64 等) /// /// 三、BoxFit 裁切模式速记: /// cover : 等比缩放填满并裁掉多余(最常用,头像/封面) /// contain : 等比缩放完整显示(会有留白) /// fill : 拉伸填满(会变形) /// fitWidth / fitHeight : 按宽/高撑满 /// /// 四、cached_network_image(3.4.1)核心价值: /// 1. 自动缓存:下载的图片写入本地缓存目录,二次加载直接读本地(不走网络), /// 对标 Web 端浏览器 HTTP 缓存; /// 2. 三段式回调:placeholder(加载中占位)/ progressIndicatorBuilder(下载进度) /// / errorWidget(失败兜底)—— 比 Image.network 的 loadingBuilder 更可靠; /// 3. 头像场景用 CachedNetworkImageProvider(url) 作为 ImageProvider, /// 同样走缓存; /// 4. 全平台含 Web(Web 端缓存能力有限,仅内存缓存,原生端走磁盘缓存)。 /// /// 五、Flutter 3.x 注意: /// Android release 包需要在 AndroidManifest.xml 加 INTERNET 权限(已加); /// 网络图片务必给 errorWidget 兜底,否则加载失败会抛异常白屏。 /// ============================================================ class ImageDemoPage extends StatelessWidget { const ImageDemoPage({super.key}); // 网络图片地址(示例) static const String _netUrl = 'https://hx.23544.com/StaticResource/2026-07-29/ranking3Position1785292048369.png'; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Image 图片组件 Demo')), body: SingleChildScrollView( padding: EdgeInsets.all(16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _SectionTitle('① 网络图片 CachedNetworkImage(带缓存 + 下载进度)'), Text( 'progressIndicatorBuilder 显示下载进度;二次加载读本地缓存秒出', style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)), ), SizedBox(height: 8.h), CachedNetworkImage( imageUrl: _netUrl, width: 300.w, height: 180.h, fit: BoxFit.cover, // 下载进度占位(与 placeholder 二选一,progress 更直观) progressIndicatorBuilder: (context, url, downloadProgress) => _buildLoadingBox( width: 300.w, height: 180.h, progress: downloadProgress.progress, ), // 兜底:加载失败(断网/404/格式错)时显示占位,不白屏 errorWidget: (context, url, error) => _buildErrorBox(width: 300.w, height: 180.h), ), SizedBox(height: 32.h), const _SectionTitle('② 本地资源图片 Image.asset'), Text( '路径必须与 pubspec.yaml 注册的 assets 一致', style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)), ), SizedBox(height: 8.h), Image.asset( 'assets/luola_logo.png', width: 150.w, height: 150.h, fit: BoxFit.contain, // 等比完整显示,不裁切 ), SizedBox(height: 32.h), const _SectionTitle('③ 圆角网络图片 ClipRRect + CachedNetworkImage'), Text( 'ClipRRect 把图片裁成圆角矩形;CachedNetworkImage 走缓存(图片本身没变,只是画出来带圆角)', style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)), ), SizedBox(height: 8.h), ClipRRect( borderRadius: BorderRadius.circular(60.r), child: CachedNetworkImage( imageUrl: _netUrl, width: 300.w, height: 180.h, fit: BoxFit.cover, // 加载中占位(转圈) placeholder: (context, url) => _buildLoadingBox( width: 300.w, height: 180.h, progress: null, ), errorWidget: (context, url, error) => _buildErrorBox(width: 300.w, height: 180.h), ), ), SizedBox(height: 32.h), const _SectionTitle( '④ 圆形网络头像 CircleAvatar + CachedNetworkImageProvider'), Text( 'CachedNetworkImageProvider 走缓存;CircleAvatar 自带圆形裁剪,常做用户头像', style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)), ), SizedBox(height: 8.h), CircleAvatar( radius: 30.w, backgroundColor: const Color(0xFFF0F2F5), backgroundImage: CachedNetworkImageProvider(_netUrl), // 头像加载失败时的兜底:显示首字母 onBackgroundImageError: (error, stackTrace) {}, child: Text( '头', style: TextStyle(fontSize: 24.sp, color: Color(0xFF999999)), ), ), ], ), ), ); } /// 加载中占位 /// progress 为 null 时显示不确定转圈;非 null 时显示百分比进度条 Widget _buildLoadingBox({ required double width, required double height, required double? progress, }) { return Container( width: width, height: height, color: const Color(0xFFF0F2F5), child: Center( child: progress == null ? SizedBox( width: 24.w, height: 24.w, child: const CircularProgressIndicator(strokeWidth: 2), ) : Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 24.w, height: 24.w, child: CircularProgressIndicator( value: progress, strokeWidth: 2, ), ), SizedBox(height: 8.h), Text( '${(progress * 100).toInt()}%', style: TextStyle(fontSize: 12.sp, color: Color(0xFF888888)), ), ], ), ), ); } /// 统一的加载失败占位图 Widget _buildErrorBox({required double width, required double height}) { return Container( width: width, height: height, color: const Color(0xFFF0F2F5), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.broken_image_outlined, size: 40.w, color: Color(0xFFB0B4BC)), SizedBox(height: 8.h), Text( '图片加载失败', style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)), ), ], ), ), ); } } /// 区块标题 class _SectionTitle extends StatelessWidget { const _SectionTitle(this.title); final String title; @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: 4.h), child: Text( title, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, color: Color(0xFF333333), ), ), ); } }