character_custom_page.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. import 'package:dotted_border/dotted_border.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_screenutil/flutter_screenutil.dart';
  4. import 'package:get/get.dart';
  5. import 'package:keyboard/base/base_page.dart';
  6. import 'package:keyboard/module/character_custom/character_custom_controller.dart';
  7. import 'package:keyboard/resource/string.gen.dart';
  8. import 'package:keyboard/utils/toast_util.dart';
  9. import '../../resource/assets.gen.dart';
  10. import '../../utils/styles.dart';
  11. import '../../widget/auto_scroll_list_view.dart';
  12. class CharacterCustomPage extends BasePage<CharacterCustomController> {
  13. const CharacterCustomPage({super.key});
  14. static start() {
  15. return Get.to(() => CharacterCustomPage());
  16. }
  17. @override
  18. bool immersive() {
  19. return true;
  20. }
  21. @override
  22. bool statusBarDarkFont() {
  23. return false;
  24. }
  25. @override
  26. Widget buildBody(BuildContext context) {
  27. return PopScope(
  28. canPop: false,
  29. onPopInvokedWithResult: (didPop, result) {
  30. if (didPop) {
  31. return;
  32. }
  33. controller.clickBack();
  34. },
  35. child: Obx(() {
  36. if (controller.currentStep.value == StepType.home) {
  37. return _buildCustomHomePage();
  38. } else {
  39. return _buildStepsPage();
  40. }
  41. }),
  42. );
  43. }
  44. // 定制首页
  45. Widget _buildCustomHomePage() {
  46. return Stack(
  47. children: [
  48. Assets.images.bgCharacterCustomHuman.image(
  49. width: double.infinity,
  50. fit: BoxFit.fill,
  51. ),
  52. SafeArea(
  53. child: Row(
  54. crossAxisAlignment: CrossAxisAlignment.center,
  55. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  56. children: [
  57. Padding(
  58. padding: EdgeInsets.only(left: 16.w),
  59. child: GestureDetector(
  60. onTap: () {
  61. controller.clickBack();
  62. },
  63. child: Assets.images.iconCharacterCustomClose.image(
  64. width: 24.w,
  65. height: 24.w,
  66. ),
  67. ),
  68. ),
  69. GestureDetector(
  70. onTap: () {
  71. controller.clickHistory();
  72. },
  73. child: Container(
  74. width: 76.r,
  75. height: 32.h,
  76. decoration: ShapeDecoration(
  77. gradient: LinearGradient(
  78. colors: [
  79. const Color(0xFF702E96),
  80. const Color(0XFF400264),
  81. ],
  82. begin: Alignment.centerLeft,
  83. end: Alignment.centerRight,
  84. ),
  85. shape: RoundedRectangleBorder(
  86. borderRadius: BorderRadius.only(
  87. topLeft: Radius.circular(16.r),
  88. bottomLeft: Radius.circular(16.r),
  89. ),
  90. ),
  91. ),
  92. child: Center(
  93. child: Text(
  94. StringName.characterCustomHistory,
  95. style: TextStyle(
  96. color: Colors.white,
  97. fontSize: 14.sp,
  98. fontWeight: FontWeight.w400,
  99. ),
  100. ),
  101. ),
  102. ),
  103. ),
  104. ],
  105. ),
  106. ),
  107. Positioned(
  108. bottom: 50.h,
  109. left: 0,
  110. right: 0,
  111. child: GestureDetector(
  112. onTap: () {
  113. controller.clickNextButton(StepType.hobbies);
  114. },
  115. child: Center(
  116. child: Assets.images.iconCharacterCustomButton.image(
  117. width: 234.w,
  118. fit: BoxFit.contain,
  119. ),
  120. ),
  121. ),
  122. ),
  123. ],
  124. );
  125. }
  126. Widget _buildStepsPage() {
  127. return Stack(
  128. children: [
  129. Assets.images.bgCharacterCustomSteps.image(
  130. width: double.infinity,
  131. fit: BoxFit.fill,
  132. ),
  133. SafeArea(
  134. child: Column(
  135. crossAxisAlignment: CrossAxisAlignment.start,
  136. children: [
  137. Padding(
  138. padding: EdgeInsets.only(left: 16.w, bottom: 48.h),
  139. child: GestureDetector(
  140. onTap: () {
  141. controller.clickBack();
  142. },
  143. child: Assets.images.iconCharacterCustomClose.image(
  144. width: 24.w,
  145. height: 24.w,
  146. ),
  147. ),
  148. ),
  149. Expanded(
  150. child: SingleChildScrollView(
  151. child: Column(
  152. mainAxisAlignment: MainAxisAlignment.center,
  153. crossAxisAlignment: CrossAxisAlignment.center,
  154. children: [
  155. controller.currentStep.value == StepType.hobbies
  156. ? Assets.images.iconCharacterCustomStepOneTitle.image(
  157. fit: BoxFit.cover,
  158. width: 209.w,
  159. )
  160. : controller.currentStep.value == StepType.characters
  161. ? Assets.images.iconCharacterCustomStepTwoTitle.image(
  162. fit: BoxFit.cover,
  163. width: 168.w,
  164. )
  165. : Assets.images.iconCharacterCustomStepThreeTitle
  166. .image(fit: BoxFit.cover, width: 207.w),
  167. Container(
  168. margin: EdgeInsets.only(
  169. left: 16.w,
  170. right: 16.w,
  171. top: 24.h,
  172. ),
  173. width: double.infinity,
  174. decoration: ShapeDecoration(
  175. color: Colors.white,
  176. shape: RoundedRectangleBorder(
  177. borderRadius: BorderRadius.circular(20.r),
  178. ),
  179. shadows: [
  180. BoxShadow(
  181. color: Color(0x66D788FF),
  182. blurRadius: 10.r,
  183. offset: Offset(0, 0),
  184. spreadRadius: 0,
  185. ),
  186. ],
  187. ),
  188. child: Column(
  189. children: [
  190. Container(
  191. padding: EdgeInsets.symmetric(horizontal: 26.w),
  192. decoration: BoxDecoration(
  193. image: DecorationImage(
  194. image:
  195. Assets.images.bgCharacterCustomStepsDesc
  196. .provider(),
  197. fit: BoxFit.fill,
  198. ),
  199. ),
  200. child: Text(
  201. StringName.characterCustomStepsDesc,
  202. style: TextStyle(
  203. color: const Color(0xFFAD88EB),
  204. fontSize: 12.sp,
  205. fontWeight: FontWeight.w400,
  206. ),
  207. ),
  208. ),
  209. SizedBox(height: 24.h),
  210. Row(
  211. mainAxisAlignment: MainAxisAlignment.center,
  212. crossAxisAlignment: CrossAxisAlignment.center,
  213. children: [
  214. Text(
  215. '第${controller.currentStep.value.value}步',
  216. style: TextStyle(
  217. color: const Color(0xFF755AAB),
  218. fontSize: 12.sp,
  219. fontWeight: FontWeight.w500,
  220. ),
  221. ),
  222. Text(
  223. " | ",
  224. style: TextStyle(
  225. color: const Color(0xFF755AAB),
  226. fontSize: 12.sp,
  227. fontWeight: FontWeight.w500,
  228. ),
  229. ),
  230. Opacity(
  231. opacity: 0.60,
  232. child: Text(
  233. '共3步',
  234. style: TextStyle(
  235. color: const Color(0xFF755BAB),
  236. fontSize: 12.sp,
  237. fontWeight: FontWeight.w500,
  238. ),
  239. ),
  240. ),
  241. ],
  242. ),
  243. if (controller.currentStep.value ==
  244. StepType.hobbies)
  245. _buildHobbiesPage(),
  246. if (controller.currentStep.value ==
  247. StepType.characters)
  248. _buildCharacterPage(),
  249. if (controller.currentStep.value ==
  250. StepType.inputName)
  251. _buildInputNamePage(),
  252. ],
  253. ),
  254. ),
  255. ],
  256. ),
  257. ),
  258. ),
  259. ],
  260. ),
  261. ),
  262. ],
  263. );
  264. }
  265. Widget _buildSelectionPage({
  266. required String title,
  267. required String subtitle,
  268. required List<dynamic> items,
  269. required RxList<dynamic> selectedLabels,
  270. required Function(dynamic) onSelected,
  271. required bool isCustomEnabled,
  272. required VoidCallback onCustomClick,
  273. required VoidCallback nextClick,
  274. required bool isShowEmoji,
  275. }) {
  276. return Obx(() {
  277. int rowCount = 3; // 3 行
  278. int columnCount = (items.length / rowCount).ceil(); // 计算列数
  279. return Column(
  280. children: [
  281. Padding(
  282. padding: EdgeInsets.only(top: 15.h),
  283. child: Text(
  284. title,
  285. style: TextStyle(
  286. color: const Color(0xFF755BAB),
  287. fontSize: 18.sp,
  288. fontWeight: FontWeight.w500,
  289. ),
  290. ),
  291. ),
  292. Text(
  293. subtitle,
  294. style: TextStyle(
  295. color: const Color(0xFF755BAB).withOpacity(0.6),
  296. fontSize: 12.sp,
  297. fontWeight: FontWeight.w500,
  298. ),
  299. ),
  300. Container(
  301. margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
  302. height: 180.h,
  303. child: AutoScrollListView(
  304. itemCount: columnCount, // 列数
  305. scrollDirection: Axis.horizontal,
  306. itemBuilder: (context, columnIndex) {
  307. // 3 个 item
  308. int startIndex = columnIndex * rowCount;
  309. List<dynamic> columnItems =
  310. items.skip(startIndex).take(rowCount).toList();
  311. return Column(
  312. mainAxisSize: MainAxisSize.min,
  313. children:
  314. columnItems.map((item) {
  315. final emoji = item.emoji ?? "";
  316. final name = item.name ?? "";
  317. return Padding(
  318. padding: EdgeInsets.symmetric(
  319. vertical: 4.h,
  320. horizontal: 4.w,
  321. ),
  322. child: Obx(() {
  323. bool isSelected = selectedLabels.any(
  324. (selected) => selected.name == item.name,
  325. );
  326. return ChoiceChip(
  327. label: Text(
  328. isShowEmoji ? "$emoji$name" : name,
  329. style: TextStyle(
  330. color:
  331. isSelected
  332. ? Colors.white
  333. : const Color(0xFF755BAB),
  334. fontSize: 14.sp,
  335. fontWeight: FontWeight.w400,
  336. ),
  337. ),
  338. showCheckmark: false,
  339. selected: isSelected,
  340. selectedColor: const Color(0xFFB782FF),
  341. backgroundColor: Colors.white,
  342. shape: RoundedRectangleBorder(
  343. side: BorderSide(
  344. width: 1.w,
  345. color: const Color(0x4C755BAB),
  346. ),
  347. borderRadius: BorderRadius.circular(31.r),
  348. ),
  349. onSelected: (selected) {
  350. onSelected(item);
  351. },
  352. );
  353. }),
  354. );
  355. }).toList(),
  356. );
  357. },
  358. ),
  359. ),
  360. Visibility(
  361. visible: isCustomEnabled,
  362. replacement: SizedBox(width: 115.w, height: 36.h),
  363. child: GestureDetector(
  364. onTap: onCustomClick,
  365. child: Container(
  366. margin: EdgeInsets.only(top: 3.h),
  367. child: DottedBorder(
  368. color: const Color(0xFFC9C2DB),
  369. strokeWidth: 1.0.w,
  370. borderType: BorderType.RRect,
  371. radius: Radius.circular(20.r),
  372. child: Container(
  373. width: 115.w,
  374. height: 33.h,
  375. alignment: Alignment.center,
  376. child: Row(
  377. mainAxisAlignment: MainAxisAlignment.center,
  378. children: [
  379. Assets.images.iconCharacterCustomPlus.image(
  380. width: 18.w,
  381. height: 18.w,
  382. ),
  383. Text(
  384. StringName.characterCustomCustomizable,
  385. style: TextStyle(
  386. color: const Color(0xFFC9C2DB),
  387. fontSize: 14.sp,
  388. fontWeight: FontWeight.w500,
  389. ),
  390. ),
  391. ],
  392. ),
  393. ),
  394. ),
  395. ),
  396. ),
  397. ),
  398. Container(
  399. margin: EdgeInsets.only(top: 107.h, bottom: 32.h),
  400. child: _buildNextButton(
  401. isEnable: selectedLabels.isNotEmpty,
  402. onTap: nextClick,
  403. ),
  404. ),
  405. ],
  406. );
  407. });
  408. }
  409. Widget _buildNextButton({required VoidCallback onTap, required isEnable}) {
  410. return GestureDetector(
  411. onTap: () {
  412. onTap();
  413. },
  414. child: Container(
  415. width: 220.w,
  416. height: 48.h,
  417. decoration:
  418. isEnable
  419. ? Styles.getActivateButtonDecoration(31.r)
  420. : Styles.getInactiveButtonDecoration(31.r),
  421. child: Center(
  422. child: Text(
  423. '下一步',
  424. style: TextStyle(
  425. color: Colors.white,
  426. fontSize: 16.sp,
  427. fontWeight: FontWeight.w500,
  428. ),
  429. ),
  430. ),
  431. ),
  432. );
  433. }
  434. // 选择爱好页面
  435. Widget _buildHobbiesPage() {
  436. return _buildSelectionPage(
  437. title: StringName.characterCustomHobbiesTitle,
  438. subtitle:
  439. "(最多选择${controller.currentCharacterCustomConfig?.maxHobbyNum ?? 3}个)",
  440. items: controller.hobbiesLabelsList,
  441. selectedLabels: controller.hobbiesSelectLabels,
  442. isShowEmoji: true,
  443. onSelected: (name) {
  444. controller.selectHobby(name);
  445. },
  446. isCustomEnabled:
  447. controller.currentCharacterCustomConfig?.customHobby == true,
  448. onCustomClick: () {
  449. controller.clickHobbiesCustom();
  450. },
  451. nextClick: () {
  452. controller.clickHobbiesNext();
  453. },
  454. );
  455. }
  456. // 选择性格页面
  457. Widget _buildCharacterPage() {
  458. return _buildSelectionPage(
  459. title: StringName.characterCustomcharacterTitle,
  460. subtitle:
  461. "(最多选择${controller.currentCharacterCustomConfig?.maxCharacterNum ?? 3}个)",
  462. items: controller.characterLabelsList,
  463. selectedLabels: controller.characterSelectLabels,
  464. isShowEmoji: true,
  465. onSelected: (character) {
  466. controller.selectCharacter(character);
  467. },
  468. isCustomEnabled:
  469. controller.currentCharacterCustomConfig?.customCharacter == true,
  470. onCustomClick: () {
  471. controller.clickCharacterCustom();
  472. },
  473. nextClick: () {
  474. controller.clickCharacterNext();
  475. },
  476. );
  477. }
  478. // 输入名字页面
  479. Widget _buildInputNamePage() {
  480. return Obx(() {
  481. return Column(
  482. children: [
  483. Padding(
  484. padding: EdgeInsets.only(top: 15.h),
  485. child: Text(
  486. StringName.characterCustomNameTitle,
  487. style: TextStyle(
  488. color: const Color(0xFF755BAB),
  489. fontSize: 18.sp,
  490. fontWeight: FontWeight.w500,
  491. ),
  492. ),
  493. ),
  494. Text(
  495. "人设名称(最多5个字)",
  496. style: TextStyle(
  497. color: Color(0xFF755BAB).withValues(alpha: 0.6),
  498. fontSize: 12.sp,
  499. fontWeight: FontWeight.w500,
  500. ),
  501. ),
  502. Container(
  503. margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
  504. alignment: Alignment.center,
  505. child: Container(
  506. height: 48.h,
  507. alignment: Alignment.center,
  508. decoration: ShapeDecoration(
  509. color: const Color(0xFFF5F4F9),
  510. shape: RoundedRectangleBorder(
  511. borderRadius: BorderRadius.circular(31.r),
  512. ),
  513. ),
  514. child: TextField(
  515. // maxLength: maxLength,
  516. maxLines: null,
  517. expands: true,
  518. textAlign: TextAlign.center,
  519. textAlignVertical: TextAlignVertical.center,
  520. onChanged: (value) {
  521. controller.currentNameValue.value = value;
  522. },
  523. decoration: InputDecoration(
  524. counterText: "",
  525. hintText: StringName.characterCustomNameHint,
  526. hintStyle: TextStyle(color: Colors.black.withAlpha(66)),
  527. border: OutlineInputBorder(
  528. borderRadius: BorderRadius.circular(31.r),
  529. borderSide: BorderSide.none, // 移除边框线
  530. ),
  531. filled: true,
  532. fillColor: const Color(0xFFF5F4F9),
  533. ),
  534. ),
  535. ),
  536. ),
  537. Container(
  538. margin: EdgeInsets.only(top: 44.h, bottom: 32.h),
  539. child: _buildNextButton(
  540. isEnable: controller.currentNameValue.trim().isNotEmpty,
  541. onTap: () {
  542. controller.clickInputNameNext();
  543. },
  544. ),
  545. ),
  546. ],
  547. );
  548. });
  549. }
  550. }