keyboard_guide_page.dart 14 KB

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