Files
App.K12.Student.Miniapp_Flu…/lib/pages/demo/image_demo.dart
T
2026-09-04 15:22:20 +08:00

226 lines
8.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
),
),
);
}
}