flutter架 构项目基础
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user