keyboard_guide_page.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import 'package:keyboard/module/keyboard_guide/keyboard_guide_controller.dart';
  5. import 'package:keyboard/router/app_pages.dart';
  6. import 'package:keyboard/utils/toast_util.dart';
  7. import 'package:keyboard/widget/platform_util.dart';
  8. import 'package:lottie/lottie.dart';
  9. import '../../base/base_page.dart';
  10. import '../../data/bean/keyboard_guide_msg.dart';
  11. import '../../resource/assets.gen.dart';
  12. import '../../resource/colors.gen.dart';
  13. import '../../resource/string.gen.dart';
  14. import '../../utils/clipboard_util.dart';
  15. import '../../utils/url_launcher_util.dart';
  16. import '../../widget/app_lifecycle_widget.dart';
  17. import '../intimacy_scale/intimacy_scale_page.dart';
  18. import 'enums/keyboard_guide_msg_type.dart';
  19. /// 键盘引导页面
  20. class KeyboardGuidePage extends BasePage<KeyboardGuidePageController> {
  21. const KeyboardGuidePage({super.key});
  22. /// 跳转到键盘引导页
  23. static void start() {
  24. Get.toNamed(RoutePath.keyboardGuide);
  25. }
  26. /// 跳转并关闭当前页
  27. static void startAndOffMe() {
  28. Get.offNamed(RoutePath.keyboardGuide);
  29. }
  30. @override
  31. immersive() {
  32. return false;
  33. }
  34. @override
  35. Widget buildBody(BuildContext context) {
  36. return Scaffold(
  37. backgroundColor: backgroundColor(),
  38. body: AppLifecycleWidget(
  39. onAppLifecycleCallback: (isForeground) {
  40. // 完成教程
  41. controller.setNotFirstShowKeyboardTutorial();
  42. if (isForeground) {
  43. // 切换到前台时,重新检查设置,更新按钮状态
  44. controller.checkSetting();
  45. // 如果选择为默认键盘了,则尝试显示引导弹窗
  46. if (controller.isDefaultKeyboard.value) {
  47. controller.showGuideOverlayDialog();
  48. }
  49. }
  50. },
  51. child: Column(
  52. children: [
  53. // 标题栏
  54. _buildTitleBar(),
  55. // 消息列表
  56. Expanded(flex: 1, child: _buildContent()),
  57. // 底部输入栏
  58. _buildBottomInput(),
  59. ],
  60. ),
  61. ),
  62. );
  63. }
  64. // 标题栏
  65. Widget _buildTitleBar() {
  66. return Container(
  67. color: backgroundColor(),
  68. height: kToolbarHeight,
  69. padding: EdgeInsets.symmetric(horizontal: 16.0),
  70. child: Row(
  71. children: [
  72. // 返回按钮
  73. GestureDetector(
  74. onTap: controller.clickBack,
  75. child: Assets.images.iconMineBackArrow.image(
  76. width: 24.w,
  77. height: 24.h,
  78. ),
  79. ),
  80. // 标题
  81. Expanded(
  82. child: Container(
  83. alignment: Alignment.center,
  84. child: Text("", style: const TextStyle(fontSize: 18)),
  85. ),
  86. ),
  87. // 右侧按钮
  88. GestureDetector(
  89. onTap: () async {
  90. bool result = await UrlLauncherUtil.openWeChat();
  91. if (!result) {
  92. ToastUtil.show(StringName.keyboardGuideWechatNotInstall);
  93. }
  94. },
  95. child: Container(
  96. padding: EdgeInsets.only(
  97. left: 12.w,
  98. right: 14.w,
  99. top: 10.w,
  100. bottom: 10.w,
  101. ),
  102. decoration: BoxDecoration(
  103. image: DecorationImage(
  104. image: Assets.images.bgGoApp.provider(),
  105. fit: BoxFit.fill,
  106. ),
  107. ),
  108. child: Row(
  109. children: [
  110. Assets.images.iconWechat.image(height: 22.w, width: 22.w),
  111. SizedBox(width: 1.0),
  112. Text(
  113. StringName.keyboardGuideGoWechat,
  114. style: TextStyle(
  115. color: ColorName.black80,
  116. fontSize: 12,
  117. fontWeight: FontWeight.w400,
  118. ),
  119. ),
  120. ],
  121. ),
  122. ),
  123. ),
  124. ],
  125. ),
  126. );
  127. }
  128. /// 内容
  129. Widget _buildContent() {
  130. return Obx(() {
  131. // 选择了默认键盘,显示聊天列表
  132. if (controller.isDefaultKeyboard.value) {
  133. return _buildChatList();
  134. } else {
  135. // 未选择,显示引导动画
  136. return _buildGuideAnimation();
  137. }
  138. });
  139. }
  140. /// 引导动画
  141. Widget _buildGuideAnimation() {
  142. Widget animationWidget;
  143. if (PlatformUtil.isIOS) {
  144. animationWidget = Lottie.asset(
  145. Assets.anim.animKeyboardFloatingWindowChooseKeyboardIos,
  146. repeat: true,
  147. );
  148. } else if (PlatformUtil.isAndroid) {
  149. animationWidget = Lottie.asset(
  150. Assets.anim.animKeyboardFloatingWindowChooseKeyboardAndroid,
  151. repeat: true,
  152. );
  153. } else {
  154. animationWidget = SizedBox.shrink();
  155. }
  156. return Container(child: animationWidget);
  157. }
  158. /// 聊天列表
  159. Widget _buildChatList() {
  160. return Obx(() {
  161. return ListView.builder(
  162. controller: controller.scrollController,
  163. itemCount: controller.msgList.length,
  164. itemBuilder: (BuildContext context, int index) {
  165. KeyboardGuideMsg msg = controller.msgList[index];
  166. return _buildMsgItem(msg, index);
  167. },
  168. );
  169. });
  170. }
  171. /// 构建底部输入框
  172. Widget _buildBottomInput() {
  173. return Center(
  174. child: Column(
  175. children: [
  176. Container(
  177. color: ColorName.msgInputBar,
  178. padding: const EdgeInsets.symmetric(
  179. vertical: 11.0,
  180. horizontal: 12.0,
  181. ),
  182. child: Row(
  183. mainAxisAlignment: MainAxisAlignment.start,
  184. children: [
  185. Expanded(
  186. flex: 1,
  187. // 输入框的圆角边框
  188. child: Container(
  189. decoration: BoxDecoration(
  190. color: ColorName.white,
  191. borderRadius: BorderRadius.circular(10.0),
  192. ),
  193. child: Obx(() {
  194. return TextField(
  195. // 是否可用,选择了默认键盘时,才可用
  196. enabled: controller.isDefaultKeyboard.value,
  197. // 是否自动获取焦点
  198. autofocus: false,
  199. style: TextStyle(
  200. color: ColorName.black80,
  201. fontSize: 14.0,
  202. fontWeight: FontWeight.w500,
  203. ),
  204. // 设置光标颜色
  205. cursorColor: ColorName.inputCursor,
  206. // 光标宽度
  207. cursorWidth: 2.0,
  208. // 光标圆角
  209. cursorRadius: Radius.circular(2),
  210. // 设置按钮显示为发送
  211. textInputAction: TextInputAction.send,
  212. // 用户点击软键盘的发送按钮时,触发回调
  213. onSubmitted: (value) {
  214. var msg = controller.editingController.text;
  215. controller.sendMsg(msg);
  216. // 保持输入框焦点获取,不降下键盘
  217. controller.requestInputFocus();
  218. },
  219. // 输入框焦点
  220. focusNode: controller.inputFocusNode,
  221. // 点击外部区域,关闭软键盘
  222. // onTapUpOutside: (event) {
  223. // controller.clearInputFocus();
  224. // },
  225. // 输入框控制器
  226. controller: controller.editingController,
  227. decoration: InputDecoration(
  228. // 提示文字
  229. hintText: StringName.keyboardGuideInputHint,
  230. hintStyle: TextStyle(
  231. fontSize: 14.0,
  232. fontWeight: FontWeight.w400,
  233. color: ColorName.black40,
  234. ),
  235. // 去掉默认的边框
  236. border: InputBorder.none,
  237. // 设置输入框的内边距
  238. contentPadding: EdgeInsets.symmetric(
  239. horizontal: 9.0,
  240. vertical: 13.0,
  241. ),
  242. ),
  243. );
  244. }),
  245. ),
  246. ),
  247. ],
  248. ),
  249. ),
  250. ],
  251. ),
  252. );
  253. }
  254. /// 构建聊天气泡
  255. Widget _buildMsgBubble(KeyboardGuideMsg msg) {
  256. // 设置气泡的外边距,让气泡不易过长
  257. double marginValue = 35.0;
  258. EdgeInsets marginEdgeInsets;
  259. if (msg.isMe) {
  260. marginEdgeInsets = EdgeInsets.only(left: marginValue);
  261. } else {
  262. marginEdgeInsets = EdgeInsets.only(right: marginValue);
  263. }
  264. // 圆角大小
  265. double radiusSize = 14.0;
  266. // 背景圆角
  267. BorderRadius bgBorderRadius;
  268. if (msg.isMe) {
  269. bgBorderRadius = BorderRadius.only(
  270. topLeft: Radius.circular(radiusSize),
  271. topRight: Radius.circular(0),
  272. bottomLeft: Radius.circular(radiusSize),
  273. bottomRight: Radius.circular(radiusSize),
  274. );
  275. } else {
  276. bgBorderRadius = BorderRadius.only(
  277. topLeft: Radius.circular(0),
  278. topRight: Radius.circular(radiusSize),
  279. bottomLeft: Radius.circular(radiusSize),
  280. bottomRight: Radius.circular(0),
  281. );
  282. }
  283. // Flexible,文本超过一行时,自动换行,并且不超过最大宽度,不超过一行时,则自动包裹内容
  284. return Flexible(
  285. child: Container(
  286. padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 10.0),
  287. margin: marginEdgeInsets,
  288. decoration: BoxDecoration(
  289. color: msg.isMe ? ColorName.msgBubbleMe : ColorName.msgBubbleTa,
  290. borderRadius: bgBorderRadius,
  291. ),
  292. child: GestureDetector(
  293. onTap: () {
  294. // 复制内容到剪切板
  295. if (msg.type == KeyboardGuideMsgType.copy.type) {
  296. ClipboardUtil.copyToClipboard(msg.content);
  297. ToastUtil.show(StringName.copySuccess);
  298. } else if (msg.type == KeyboardGuideMsgType.intimacySetting.type) {
  299. // 跳转到亲密度设置页
  300. IntimacyScalePage.start();
  301. }
  302. },
  303. child: Row(
  304. // 宽高包裹内容
  305. mainAxisSize: MainAxisSize.min,
  306. // 图标和文本,垂直居中
  307. crossAxisAlignment: CrossAxisAlignment.center,
  308. children: [
  309. Flexible(
  310. // 消息文本
  311. child: Text(
  312. msg.content,
  313. style: TextStyle(
  314. fontSize: 14.0,
  315. color: ColorName.black80,
  316. fontWeight: FontWeight.w500,
  317. height: 1.5,
  318. ),
  319. softWrap: true,
  320. ),
  321. ),
  322. // 只有对方发送的,才有操作按钮
  323. if (!msg.isMe)
  324. Visibility(
  325. visible: msg.type != KeyboardGuideMsgType.normal.type,
  326. child: Padding(
  327. padding: EdgeInsets.only(left: 8.0),
  328. child: _buildMsgActionBtn(msg),
  329. ),
  330. ),
  331. ],
  332. ),
  333. ),
  334. ),
  335. );
  336. }
  337. /// 消息操作按钮
  338. Widget _buildMsgActionBtn(KeyboardGuideMsg msg) {
  339. if (msg.type == KeyboardGuideMsgType.copy.type) {
  340. return Assets.images.iconCopy.image(width: 18.w, height: 18.w);
  341. } else if (msg.type == KeyboardGuideMsgType.intimacySetting.type) {
  342. return Assets.images.iconSetting.image(width: 18.w, height: 18.w);
  343. } else {
  344. return SizedBox.shrink();
  345. }
  346. }
  347. /// 构建聊天消息列表项
  348. Widget _buildMsgItem(KeyboardGuideMsg msg, int index) {
  349. return Obx(() {
  350. Widget content;
  351. // 自己发的
  352. if (msg.isMe) {
  353. content = Row(
  354. // 如果是自己发的,则在右边
  355. mainAxisAlignment: MainAxisAlignment.end,
  356. // 顶部对齐
  357. crossAxisAlignment: CrossAxisAlignment.start,
  358. children: [
  359. // 聊天气泡
  360. _buildMsgBubble(msg),
  361. SizedBox(width: 9.w),
  362. // 头像
  363. _buildAvatar(msg),
  364. ],
  365. );
  366. } else {
  367. // 对方发的
  368. content = Row(
  369. // 如果是自己发的,则在右边
  370. mainAxisAlignment: MainAxisAlignment.start,
  371. // 顶部对齐
  372. crossAxisAlignment: CrossAxisAlignment.start,
  373. children: [
  374. // 头像
  375. _buildAvatar(msg),
  376. SizedBox(width: 9.w),
  377. // 聊天气泡
  378. _buildMsgBubble(msg),
  379. ],
  380. );
  381. }
  382. bool isTargetGuildMsg = controller.guideMsgIndex.value == index;
  383. return Container(
  384. margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.0.h),
  385. child:
  386. isTargetGuildMsg
  387. ? Container(key: controller.guideMsgGlobalKey, child: content)
  388. : content,
  389. );
  390. });
  391. }
  392. /// 构建头像
  393. Widget _buildAvatar(KeyboardGuideMsg msg) {
  394. double avatarSize = 36.0;
  395. return CircleAvatar(
  396. radius: 20,
  397. child:
  398. msg.isMe
  399. ? Assets.images.iconDefaultAvatar.image(
  400. height: avatarSize,
  401. width: avatarSize,
  402. )
  403. : Assets.images.iconTaAvatar.image(
  404. height: avatarSize,
  405. width: avatarSize,
  406. ),
  407. );
  408. }
  409. }