一个简单图片素材网站
登录和注册页滑动切换使用的是TabBar
+PageView
完成
首先初始化TabBar和PageView控制器,并为其添加切换监听事件
late final _pageController;late final _tabController;final List _tabs = ['登录','注册'];@overridevoid 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));}@overridevoid dispose() {_pageController.dispose();_tabController.dispose();super.dispose();}
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的使用方法如下,重点就是页面切换事件和控制器的绑定,它的子组件就由需要滑动的页面组成,这里这样登录和注册两个
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('username');String? _password = await SpUtil.getValue('password');if (_username == username && _password == password) {print('[成功信息]:登录成功');showSuccessToast('登录成功!');Navigator.of(context).push(MaterialPageRoute(builder: (context) => const HomePage()));} else {print('[错误信息]:登录失败');showFailedToast('登录失败!');}
}
首先导入依赖
shared_preferences: ^2.0.15
static setValue(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 getValue(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();}
因为我没有找到好的图片素材接口,那个接口没有通过关机键搜索然后返回内容的接口,所以此处搜索栏没有使用通过搜索内容跳转相关内容的页面,无论输入什么都跳转至全览页
一个背景图加一个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))],);
此处只是简单使用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: ^4.0.0
通过懒汉单例构造Dio封装
static var dio;static var dioUtils;static DioUtils get instance => getInstance();static DioUtils getInstance() {return dioUtils ??= DioUtils();}
其中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(String url, FormData? param, Function(T t) onSuccess, Function(String error) onError) async {requestHttp(url,param: param,method: GET,onSuccess: onSuccess,onError: onError,);}
与Get方法一样,此处不在阐述
post(String url, FormData param, Function(T t) onSuccess, Function(String error) onError) async {requestHttp(url,param: param,method: POST,onSuccess: onSuccess,onError: onError,);}
此处建立dio对象,然后进行网络请求,最后将response.data
Json字符串进行回调,若是失败则走失败回调
static requestHttp(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 t) onSuccess, Function(String error) onError){DioUtils.instance.get(ApiPath.verticalUrl, null, (data){var baseBean = BaseImageEntityEntity.fromJson(data as Map);var verticalList = VerticalEntityEntity.fromJson(baseBean.res as Map);onSuccess(verticalList.vertical);},(error){print("【请求失败】${error.toString()}");showFailedToast('failed!');onError(error);});
}
使用的是一个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 json) => $BaseImageEntityEntityFromJson(json);Map toJson() => $BaseImageEntityEntityToJson(this);@overrideString toString() {return jsonEncode(this);}
}
这些都是它自动生成的,你每创建Bean类,它就会多一个对应的解析类,然后将添加到convert文件中
通过继承Dialog
组件实现自定义,通过通过GestureDetector
组件为图片添加点击事件,点击区域外部可取消Dialog,使用 Navigator.pop(context);
也可以取消当前Dialog
class ImageDialog extends Dialog {final String imageUrl;const ImageDialog(this.imageUrl, {super.key});@overrideWidget 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与上述无异,此处滤过
此处下载功能通过调用原生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链接