keyboard_guide_page.dart 15 KB

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