仿微信开发
项目中使用的假数据,从网络中获取。整体开发思路参考git上的一些项目。根据多个项目整理出适合自己的开发思路。
首页
需要实现的功能点
- 本地JSON数据的获取。✅
- JSON转model。✅
- 列表中每个选项的左滑。✅
- 下拉小程序。(最难的点)
- 搜索
- 右上角加好按钮弹框 需要的知识点:在开发中总结。
语言国际化/手机暗黑模式适配已经配置完毕,本地JSON文件数据获取已经搞定。项目基本搭建起来了。可以开始正常的功能/UI开发
暗黑模式适配我整理了一下:链接
列表基本样式开发。
考虑到需要用到图片缓存,所以使用了cached_network_image插件:
cached_network_image: ^3.2.0
还有屏幕适配插件:
flutter_screenutil
然后再阿里巴巴矢量图库找了几个icon,静音icon和默认头像icon。
左边头像通过Wrap组件实现网格布局。
右边通过Column套用两个Row的方式实现。
child: ListTile(
leading: leftWidget,
title: rightWidget,
),
然后用ListTile把他俩组装起来。
编译起来看下效果。
看起来效果不错。
但是少了分割线。接下来添加分割先
添加分割线
因为组件比较多,所以我准备吧分割线头像和右侧label封装一下,名字就叫做:
WXWeChatListTitle
封装完毕,运行起来也挺完美,那么接下来的问题是:
怎么在打开第二个菜单的时候关闭第一个。
网上一顿搜索,都说是要设置:
Slidable(
controller:
)
但是发现根本没有controller属性。
后来去查看官方文档,发现Slidable0.6版本之后就没有controller属性了。
关闭另外一个菜单的方法替换为以下方式。
也就是使用SlidableAutoCloseBehavior包裹ListView,探后在Slidable,设置groupTag属性,设置成一样的值,这样打开第二个第一个就会自动关闭啦。
以下是WXWeChatListTitle封装的全部代码:
class _WXWeChatListTitleState extends State<WXWeChatListTitle> {
@override
List<Widget> actionList = [];
Widget build(BuildContext context) {
actionList = _endActionChildren();
return Slidable(
groupTag:"1",
key: Key(widget.chatModel.idstr),
child: _buildChildWidget(),
// key: ValueKey(0),
endActionPane: ActionPane(
motion: ScrollMotion(),
dismissible: DismissiblePane(onDismissed: () {}),
dragDismissible: false,
extentRatio: actionList.length * 0.23,
children: actionList,
),
);
}
List<Widget> _endActionChildren() {
List<Widget> tempList = [];
/// 删除按钮
Widget deleteBtn = SlidableAction(
onPressed: (context) {
Slidable.of(context)?.close();
},
flex: 3,
backgroundColor: Color.fromRGBO(245, 56, 64, 1),
foregroundColor: Colors.white,
// icon: Icons.delete,
label: '删除',
);
/// 标记未读
Widget markUnreadBtn = SlidableAction(
// An action can be bigger than the others.
onPressed: (context) {},
flex: 4,
backgroundColor: Color.fromRGBO(20, 109, 231, 1),
foregroundColor: Colors.white,
// icon: Icons.archive,
label: '标记未读',
);
/// 不显示
Widget hideBtn = SlidableAction(
// An action can be bigger than the others.
onPressed: (context) {},
flex: 3,
backgroundColor: Color.fromRGBO(249, 135, 46, 1),
foregroundColor: Colors.white,
// icon: Icons.archive,
label: '不显示',
);
Widget focusBtn = SlidableAction(
// An action can be bigger than the others.
onPressed: (context) {},
flex: 4,
backgroundColor: Color(0xFFC7C7CB),
foregroundColor: Colors.white,
// icon: Icons.archive,
label: '不再关注',
);
if (widget.chatModel.type == '0') {
// 订��号消息、微信运动、微信支付
tempList.addAll([hideBtn,deleteBtn]);
} else if (widget.chatModel.type == '1') {
// 单聊、群聊、QQ邮箱提醒
tempList.addAll([markUnreadBtn,hideBtn,deleteBtn]);
} else {
// 公众号
tempList.addAll([focusBtn,hideBtn, deleteBtn]);
}
return tempList;
}
/// 构建分割线
Widget _buildDividerWidget() {
return Divider(
height: widget.dividerHeight,
color: widget.dividerColor,
indent: widget.dividerIndent,
endIndent: widget.dividerEndIndent,
);
}
/// 构建左侧头像
Widget _setLeftWidget() {
return WeChatListPhoto(widget.chatModel);
}
/// 构建右侧布局
Widget _setRightWidget() {
return Column(
children: [
Row(
children: [
Expanded(
child: Text(
widget.chatModel.screenName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Style.pTextColor,
),
),
),
Text(
'2022/04/24',
style: TextStyle(
color: Color(0xFFB2B2B2),
fontSize: ScreenUtil().setSp(36.0),
),
),
],
),
SizedBox(height: ScreenUtil().setHeight(9.0)),
Row(
children: [
Expanded(
child: Text(
widget.chatModel.text,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Color(0xFF9B9B9B),
fontSize: ScreenUtil().setSp(48.0),
),
),
),
Offstage(
offstage: !widget.chatModel.messageFree,
child: Image.asset(
AssetsPathManage.assetsImagesMainframe +
'AlbumMessageDisableNotifyIcon_15x15.png',
width: ScreenUtil().setWidth(45),
height: ScreenUtil().setHeight(45.0),
),
)
],
)
],
);
}
/// 构建主控件(组装)
Widget _buildChildWidget() {
return Column(
children: <Widget>[
ListTile(
leading: _setLeftWidget(),
title: _setRightWidget(),
onTap: (){
Slidable.of(context)?.close();
},
),
_buildDividerWidget(),
],
);
}
}
class WXWeChatListTitle extends StatefulWidget {
/// 分割线高度 default is 0.5
double dividerHeight = 1.0;
/// 分割线颜色 default is 0xFFD8D8D8
Color dividerColor = Color(0xFFD8D8D8);
/// 分割线相对头部偏移量 default is 0.0
double dividerIndent = ScreenUtil().setWidth(133);
/// 分割线相对尾部偏移量 default is 0.0
double dividerEndIndent = 0;
/// 聊天的Entity
MsglistEntity chatModel;
/// 初始化器
WXWeChatListTitle(this.chatModel, this.dividerHeight, this.dividerColor,
this.dividerIndent, this.dividerEndIndent);
@override
State<WXWeChatListTitle> createState() => _WXWeChatListTitleState();
}
下拉刷新,上拉加载控件添加。
我这里使用的是pull_to_refresh
这次不去百度了,直接去看官方文档,防止走歪路: 果然根据官方文档很快就写出刷新和加载控件。记得要把语言国际化功能加上,(提示文字)。
下面是完整代码:这里官方反复强调 ListView一定要作为SmartRefresher的child。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// backgroundColor: Color.fromRGBO(19, 19, 19, 1),
title: Text(S.of(context).tabbar_item_wechat),
),
body: Container(
child:SlidableAutoCloseBehavior(
child:SmartRefresher(
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
enablePullDown: true,
enablePullUp: true,
header: WaterDropHeader(),
footer: CustomFooter(
builder: (context,mode){
Widget body ;
if(mode==LoadStatus.idle){
///上拉加载
body = Text(S.of(context).listView_loadMore);
}else if(mode==LoadStatus.loading){
body = CupertinoActivityIndicator();
}else if(mode == LoadStatus.failed){
///加载失败!点击重试!
body = Text(S.of(context).listView_LoadFailed);
}else if(mode == LoadStatus.canLoading){
///松手加载更多
body = Text(S.of(context).listView_loadMore);
}else{
///没有更多数据了.
body = Text(S.of(context).listView_NoMoreData);
}
return Container(
height: 55.0,
child: Center(child:body),
);
},
),
child: ListView(
children: _getData(),
),
) ,
),
),
);
官方文档还提供有设置全局上拉刷新下拉加载控件的方法。
全局配置RefreshConfiguration,配置子树下的所有SmartRefresher表现,一般存放于MaterialApp的根部,用法和ScrollConfiguration是类似的。 另外,假如你某一个SmartRefresher表现和全局不一样的情况,你可以使用RefreshConfiguration.copyAncestor从祖先RefreshConfiguration复制属性过来并替换不为空的属性。
// 全局配置子树下的SmartRefresher,下面列举几个特别重要的属性
RefreshConfiguration(
headerBuilder: () => WaterDropHeader(), // 配置默认头部指示器,假如你每个页面的头部指示器都一样的话,你需要设置这个
footerBuilder: () => ClassicFooter(), // 配置默认底部指示器
headerTriggerDistance: 80.0, // 头部触发刷新的越界距离
springDescription:SpringDescription(stiffness: 170, damping: 16, mass: 1.9), // 自定义回弹动画,三个属性值意义请查询flutter api
maxOverScrollExtent :100, //头部最大可以拖动的范围,如果发生冲出视图范围区域,请设置这个属性
maxUnderScrollExtent:0, // 底部最大可以拖动的范围
enableScrollWhenRefreshCompleted: true, //这个属性不兼容PageView和TabBarView,如果你特别需要TabBarView左右滑动,你需要把它设置为true
enableLoadingWhenFailed : true, //在加载失败的状态下,用户仍然可以通过手势上拉来触发加载更多
hideFooterWhenNotFull: false, // Viewport不满一屏时,禁用上拉加载更多功能
enableBallisticLoad: true, // 可以通过惯性滑动触发加载更多
child: MaterialApp(
........
)
);
然后在设置语言国际化delete的地方加入RefreshLocalizations.delegate,:
MaterialApp(
localizationsDelegates: [
// 这行是关键
RefreshLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalMaterialLocalizations.delegate
],
supportedLocales: [
const Locale('en'),
const Locale('zh'),
],
localeResolutionCallback:
(Locale locale, Iterable<Locale> supportedLocales) {
//print("change language");
return locale;
},
)
等等,好像忘了一样东西,适配系统主题色!!!
在dark模式下确实有问题:如下
20220426
主要是字体颜色的问题。
接下来创建一个管理类,名字就叫做:WXDarkAndLightConfig。
import 'package:flutter/material.dart';
///系统主题色适配
import 'package:wechat_flutter_practice/constant/style.dart';
import 'package:get/get.dart';
class WXDarkAndLightConfig{
/// 颜色key
static const String wechatTitleKey = 'wechat_title_key';
static const String wechatSubtitleKey = 'wechat_subtitle_key';
static Color getColor(String key){
final colors = _SystemThemeColor._colors[key];
if (colors == null) {
return Style.pTextColor;
}
if (Get.isDarkMode == true) {
return colors[1];
}
return colors[0];
}
}
class _SystemThemeColor{
static const _colors = {
"wechat_title_key":[_titleColorDark, _titleColorLight],
"wechat_subtitle_key":[_subTitleColorDark, _subTitleColorLight],
};
static const _titleColorDark = Colors.white;
static const _titleColorLight = Style.pTextColor;
static const _subTitleColorDark = Color.fromRGBO(269,267, 268, 1);
static const _subTitleColorLight = Color.fromRGBO(269,267, 268, 1);
}
主要是作用页面中的文字/背景颜色通过WXDarkAndLightConfig获取。通过GetX插件获取当前系统主题Get.isDarkMode。
但是代码中还有不足的地方,比如
/// 颜色key
static const String wechatTitleKey = 'wechat_title_key';
static const String wechatSubtitleKey = 'wechat_subtitle_key';
static const _colors = {
"wechat_title_key":[_titleColorDark, _titleColorLight],
"wechat_subtitle_key":[_subTitleColorDark, _subTitleColorLight],
};
每加入一个颜色,这两个地方都要加上代码。感觉很繁琐,而且容易漏掉某一个。但是一时想不出怎么解决,后续在进行优化。
使用方法:感觉也很繁琐。
WXDarkAndLightConfig.getColor(WXDarkAndLightConfig.wechatTitleKey)
我想要的效果是。
WXDarkAndLightConfig.wechatTitleKey
我想到Swift中有变量的get和set方法。 刚好Dart也有。
经过修改,代码改变为下面这种。
class WXDarkAndLightConfig{
/// 颜色key
static Color get wechatTitleColor{
return _SystemThemeColor.getColor('wechat_title_key');
}
static Color get wechatSubtitleColor {
return _SystemThemeColor.getColor('wechat_subtitle_key');
}
}
class _SystemThemeColor{
static const _colors = {
"wechat_title_key":[_titleColorDark, _titleColorLight],
"wechat_subtitle_key":[_subTitleColorDark, _subTitleColorLight],
};
static Color getColor(String key){
final colors = _SystemThemeColor._colors[key];
if (colors == null) {
return Style.pTextColor;
}
if (Get.isDarkMode == true) {
return colors[1];
}
return colors[0];
}
static const _titleColorDark = Colors.white;
static const _titleColorLight = Style.pTextColor;
static const _subTitleColorDark = Color.fromRGBO(269,267, 268, 1);
static const _subTitleColorLight = Color.fromRGBO(269,267, 268, 1);
}
这样就可以直接:
WXDarkAndLightConfig.wechatTitleKey
完美!
接下来做输入框:
输入框可能困难的是动画怎么过度到搜索页,想到了 Hero 动画。试试看。
网上一堆查找,发现没有适合的第三方组建。那么自己动手做吧。
20220427
今天遇到了两个问题以及解决方法: juejin.cn/post/709118… 搜索框的搭建,导航条的搭建. 这两个目前已经完成。下一步
- 着手搭建点击右上角加号按钮后的菜单。
- 搭建下拉展示最近使用的小程序功能。
- ThemeData对象的学习,该属性影响着整个项目中的系统主题色适配。需要把该对象的每个属性都尝试修改一下,看看影响的点。 下图是当前的开发成果: