一个简单的图片素材网站
- 效果视频
- 登录注册页
- 效果图
- UI
- 初始化
- TabBar
- PageView
- 组合
- 登录
- 账号输入
- 按键处理
- SharedPreferences封装
- 保存数据
- 取出数据
- 清除缓冲内容
- 搜索栏
- 效果图
- UI
- 首页
- 效果图
- UI
- Dio网络请求
- Dio单例封装
- 构造Dio对象
- Get
- Post
- Response
- 使用
- 解析Json
- 图片阅览
- UI
- Dialog
- 下载
- UI
- 调用浏览器进行下载
- Git
效果视频
一个简单图片素材网站
登录注册页
效果图
UI
登录和注册页滑动切换使用的是TabBar
+PageView
完成
初始化
首先初始化TabBar和PageView控制器,并为其添加切换监听事件
late final _pageController;
late final _tabController;
final List<String> _tabs = <String>['登录','注册'];
@override
void initState() {
_pageController = PageController();
_tabController = TabController(length: _tabs.length, vsync: this);
super.initState();
}
void _changeTab(int index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.ease);
}
void _onPageChanged(int index) {
_tabController.animateTo(index, duration: const Duration(milliseconds: 300));
}
@override
void dispose() {
_pageController.dispose();
_tabController.dispose();
super.dispose();
}
TabBar
TabBar的使用方法如下,重点就是点击事件和控制器的绑定
Widget navBar = TabBar(
//选中的颜色
labelColor: Colors.white,
labelStyle: const TextStyle(color: Colors.white, fontSize: 16),
//未选中的颜色
unselectedLabelColor: Colors.black,
unselectedLabelStyle: const TextStyle(color: Colors.black, fontSize: 16),
//去掉下划线
indicator: const BoxDecoration(),
controller: _tabController,
onTap: _changeTab,
tabs: _tabs.map((e) => Tab(text: e)).toList(),
);
PageView
PageView的使用方法如下,重点就是页面切换事件和控制器的绑定,它的子组件就由需要滑动的页面组成,这里这样登录和注册两个
Widget navViews = SizedBox(
width: 500.0,
height: 320.0,
child: PageView(
controller: _pageController,
onPageChanged: _onPageChanged,
children: [
ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: const LoginPage()
),
ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: const RegisterPage()
)
],
),
);
组合
最外层插背景图片,并铺满全屏,下面就控制登录、注册界面在屏幕左方
return Scaffold(
body: Container(
decoration: bg,
width: double.infinity,
height: double.infinity,
child:Align(
alignment: Alignment.centerLeft,
child: Wrap(
children:[
Container(
margin: const EdgeInsets.only(left: 100.0),
child: Column(
children: [
Container(
width: 200.0,
decoration: gradient,
child: navBar,
),
const SizedBox(height: 10.0),
navViews
],
),
)
]
),
),
)
);
登录
登录与注册一致,此处以登录为例子,一个简单的表单和缓存记录比对,通过shared_preferences
这个库对注册数据进行缓存,然后登录进行读取,从而进行判断
账号输入
账号和密码差不多,以账号为例子;同样绑定控制器和焦点节点,在尾部添加一个清空文本按钮,当内容不为空时出现,反之,隐藏;validator
里面为不满足你所设置的条件,则下方弹出一行提示(内容自定义),基本Material风格都这样设计的
///用户名
Widget username_input = TextFormField(
maxLines: 1,
controller: _usernameController,
focusNode: _focusNodeUserName,
decoration: InputDecoration(
icon:const Icon(Icons.people_alt_outlined),
labelText: '账号',
suffixIcon: (_isShowClear)
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
// 清空文本框的内容
_usernameController.clear();
})
: null),
validator: (value) {
if(value == null || value.isEmpty){
return '用户名不能为空';
}else{
return null;
}
},
onSaved: (String? data) {
_username = data.toString();
},
autovalidateMode: AutovalidateMode.onUserInteraction,
);
按键处理
重点在于点击事件那里,可以加一个表单验证,就是加入你输入的内容为空时,不满足上述validator
所设置的条件,就可以不执行方法体的内容,因为dart判空机制,所以前面需要加一个!
///登录按钮
Widget loginButton = Container(
width: 150.0,
height: 40.0,
decoration: gradient,
child:ElevatedButton(
style: ButtonStyle(
//去除阴影
elevation: MaterialStateProperty.all(0),
//将按钮背景设置为透明
backgroundColor: MaterialStateProperty.all(Colors.transparent),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
login(_username, _password,context);
//testDio();
}
},
child: const Text('登录')
)
);
通过按钮被按下后,通过键值对获取SharedPreferences
的缓存内容,然后与输入的进行判断,并通过Toast进行提示
void login(String username,String password,BuildContext context) async{
String? _username = await SpUtil.getValue<String>('username');
String? _password = await SpUtil.getValue<String>('password');
if (_username == username && _password == password) {
print('[成功信息]:登录成功');
showSuccessToast('登录成功!');
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const HomePage()));
} else {
print('[错误信息]:登录失败');
showFailedToast('登录失败!');
}
}
SharedPreferences封装
首先导入依赖
shared_preferences: ^2.0.15
保存数据
static setValue<T>(String key, T value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
switch (T) {
case String:
prefs.setString(key, value as String);
break;
case int:
prefs.setInt(key, value as int);
break;
case bool:
prefs.setBool(key, value as bool);
break;
case double:
prefs.setDouble(key, value as double);
break;
}
}
取出数据
static Future<T> getValue<T>(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
late T res;
switch (T) {
case String:
res = prefs.getString(key) as T;
break;
case int:
res = prefs.getInt(key) as T;
break;
case bool:
res = prefs.getBool(key) as T;
break;
case double:
res = prefs.getDouble(key) as T;
break;
}
return res;
}
清除缓冲内容
static void removeCache(String key) async{
SharedPreferences sp = await SharedPreferences.getInstance();
sp.remove(key);
}
static void removeAllCache() async{
SharedPreferences sp = await SharedPreferences.getInstance();
sp.clear();
}
搜索栏
因为我没有找到好的图片素材接口,那个接口没有通过关机键搜索然后返回内容的接口,所以此处搜索栏没有使用通过搜索内容跳转相关内容的页面,无论输入什么都跳转至全览页
效果图
UI
一个背景图加一个Colum布局
搜索条通过InputDecoration
包含了一个尾部跳转按钮,border: InputBorder.none
此句可以去除输入框下划线
var inputStyle = InputDecoration(
suffixIcon: IconButton(
onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const AllImage()));
},
icon: const Icon(Icons.g_mobiledata_outlined)),
icon:const Icon(Icons.search),
hintText: 'Search for all image',
border: InputBorder.none);
然后使用Card布局,在添加一点间距,圆角角度加大一点,就完成了一个搜索条
/// 搜索框
Widget searchBar = Container(
height: 60.0,
width: 800.0,
//padding: const EdgeInsets.all(20.0),
child: Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(30.0))),
color: Colors.white,
child: Container(
alignment: Alignment.center,
margin: const EdgeInsets.only(left: 20.0),
child:TextField(decoration: inputStyle,maxLines: 1,))
)
);
上图效果最后由如下代码组装
Widget topList = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(first_line_text,style: getTextStyle(28.0, FontWeight.bold, Colors.white)),
const SizedBox(height: 20.0,),
Text(second_line_text,style: getTextStyle(14.0, FontWeight.bold, Colors.white)),
const SizedBox(height: 20.0,),
searchBar,
const SizedBox(height: 20.0,),
Text(third_line_text,style: getTextStyle(14.0, FontWeight.bold, Colors.white))
],
);
首页
效果图
UI
此处只是简单使用GridView进行图片内容展示
Widget bottomArea = Container(
height: 750,
margin: const EdgeInsets.only(left: 100.0,right: 100.0),
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 6,
mainAxisSpacing: 20.0,
crossAxisSpacing: 20.0,
childAspectRatio: 0.7,
children: List.generate(imageList.length, (index) => getImageChile(imageList[index],context)),
),
);
将获取的网络图片通过Image.network
进行展示,并设置未显示时,显示loading
样式的progress占位,并使用GestureDetector
为图片添加点击事件
GestureDetector getImageChile(ImageBeanEntity entity,BuildContext context){
return GestureDetector(
onTap: (){
DialogUtil.showImageDialog(context, entity.img);
},
child: Image.network(
entity.img,
errorBuilder: (context,error,stackTrace){
return const CircularProgressIndicator();
},
loadingBuilder: (context,child,progress){
if(progress == null)return child;
return Container(
alignment: Alignment.center,
child: CircularProgressIndicator(
value: progress.expectedTotalBytes != null ?
progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null,
),
);
}
),
);
}
Dio网络请求
本例通过Dio库进行网络请求访问,添加如下依赖
dio: ^4.0.0
Dio单例封装
通过懒汉单例构造Dio封装
static var dio;
static var dioUtils;
static DioUtils get instance => getInstance();
static DioUtils getInstance() {
return dioUtils ??= DioUtils();
}
构造Dio对象
其中baseUrl
为接口前缀,例如http://172.0.0.1/?limit=12
这个示例接口,其中http://172.0.0.1
就为baseUrl
,此处只做为示例,具体根据开发需求和自己喜好,也可动态配置
// 创建 dio 实例对象
static Dio createInstance() {
if (dio == null) {
/// 全局属性:请求前缀、连接超时时间、响应超时时间
var options = BaseOptions(
// responseType: ResponseType.json,
baseUrl: ApiPath.baseUrl,
connectTimeout: _connectTimeout,
receiveTimeout: _receiveTimeout,
sendTimeout: _sendTimeout,
);
dio = Dio(options);
}
return dio;
}
// 清空 dio 对象
static clear() {
dio = null;
}
Get
通过传入接口和参数,然后将请求结果通过回调函数进行回调,此处指定为Get
方式请求
get<T>(String url, FormData? param, Function(T t) onSuccess, Function(String error) onError) async {
requestHttp<T>(
url,
param: param,
method: GET,
onSuccess: onSuccess,
onError: onError,
);
}
Post
与Get方法一样,此处不在阐述
post<T>(String url, FormData param, Function(T t) onSuccess, Function(String error) onError) async {
requestHttp<T>(
url,
param: param,
method: POST,
onSuccess: onSuccess,
onError: onError,
);
}
Response
此处建立dio对象,然后进行网络请求,最后将response.data
Json字符串进行回调,若是失败则走失败回调
static requestHttp<T>(String url, {param, method, required Function(T map) onSuccess, required Function(String error) onError,}) async {
dio = createInstance();
try {
Response response = await dio.request(
url,
data: param,
options: Options(method: method));
if (response.statusCode == 200) {
onSuccess(response.data);
} else {
onError("【statusCode】${response.statusCode}");
}
} on DioError catch (e) {
/// 打印请求失败相关信息
print("【请求出错1】${e.toString()}");
onError(e.toString());
}
}
使用
此处处理网络请求返回回来的数据
void getImageData(Function(List<ImageBeanEntity> t) onSuccess, Function(String error) onError){
DioUtils.instance.get(ApiPath.verticalUrl, null, (data){
var baseBean = BaseImageEntityEntity.fromJson(data as Map<String, dynamic>);
var verticalList = VerticalEntityEntity.fromJson(baseBean.res as Map<String, dynamic>);
onSuccess(verticalList.vertical);
},(error){
print("【请求失败】${error.toString()}");
showFailedToast('failed!');
onError(error);
});
}
解析Json
使用的是一个JsonToDartBeanAction
插件进行解析,只需要通过输入需要解析的Json串,他会自动生成bean类和转换类
以此类为例,我传入的JSON串如下
{
"msg":"success",
"res":Object{...},
"code":0
}
然后他自动生成Bean类以及JSON解析和转换类
@JsonSerializable()
class BaseImageEntityEntity {
late String msg;
dynamic res;
late int code;
BaseImageEntityEntity();
factory BaseImageEntityEntity.fromJson(Map<String, dynamic> json) => $BaseImageEntityEntityFromJson(json);
Map<String, dynamic> toJson() => $BaseImageEntityEntityToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}
这些都是它自动生成的,你每创建Bean类,它就会多一个对应的解析类,然后将添加到convert文件中
图片阅览
UI
Dialog
通过继承Dialog
组件实现自定义,通过通过GestureDetector
组件为图片添加点击事件,点击区域外部可取消Dialog,使用 Navigator.pop(context);
也可以取消当前Dialog
class ImageDialog extends Dialog {
final String imageUrl;
const ImageDialog(this.imageUrl, {super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: double.infinity,
margin: const EdgeInsets.all(100.0),
padding: const EdgeInsets.all(50.0),
decoration: const BoxDecoration(
color: Color(0x66000000),
borderRadius: BorderRadius.all(Radius.circular(15.0))),
child: GestureDetector(
onTap: () {
// downloadImage();
DialogUtil.showDownloadDialog(context, imageUrl);
},
child: Image.network(imageUrl,
errorBuilder: (context, error, stackTrace) {
return const CircularProgressIndicator();
}, loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return Container(
alignment: Alignment.center,
child: CircularProgressIndicator(
value: progress.expectedTotalBytes != null
? progress.cumulativeBytesLoaded /
progress.expectedTotalBytes!
: null,
),
);
})));
}
}
下载
下载Dialog与上述无异,此处滤过
UI
调用浏览器进行下载
此处下载功能通过调用原生html
的a
标签进行下载,但是需要引入一个库,前者是使用html
的库,后者是使用http
的库
universal_html: ^1.2.1
http: ^0.13.1
引入依赖
import 'package:universal_html/html.dart' as html;
import 'package:http/http.dart' as http;
首先访问图片URL,然后将其进行编码,最后使用html.AnchorElement
创建html
标签,然后以当前时间为下载完成图片的名字
void downloadImage() async {
try {
final http.Response response = await http.get(Uri.parse(imageUrl));
final data = response.bodyBytes;
final base64data = base64Encode(data);
final a = html.AnchorElement(href: 'data:image/jpeg;base64,$base64data');
String imageName = 'download_in_${DateTime.now().toString()}.png';
a.download = imageName;
a.click();
a.remove();
} catch (e) {
print(e.toString());
}
}
Git
Git链接