flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
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),
),
),
);
}
}