character_custom_page.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  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 Future<void> start({StepType? step}) async {
  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: StaggeredAutoScrollListView(
  304. itemCount: items.length,
  305. isAutoScrolling: true,
  306. itemBuilder: (context, index) {
  307. final item = items[index];
  308. final emoji = item.emoji ?? "";
  309. final name = item.name ?? "";
  310. return Padding(
  311. padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 4.w),
  312. child: Obx(() {
  313. bool isSelected = selectedLabels.any(
  314. (selected) => selected.name == item.name,
  315. );
  316. return ChoiceChip(
  317. label: Text(
  318. isShowEmoji ? "$emoji$name" : name,
  319. style: TextStyle(
  320. color: isSelected ? Colors.white : const Color(0xFF755BAB),
  321. fontSize: 14.sp,
  322. fontWeight: FontWeight.w400,
  323. ),
  324. ),
  325. showCheckmark: false,
  326. selected: isSelected,
  327. selectedColor: const Color(0xFFB782FF),
  328. backgroundColor: Colors.white,
  329. shape: RoundedRectangleBorder(
  330. side: BorderSide(
  331. width: 1.w,
  332. color: const Color(0x4C755BAB),
  333. ),
  334. borderRadius: BorderRadius.circular(31.r),
  335. ),
  336. onSelected: (selected) {
  337. onSelected(item);
  338. },
  339. );
  340. }),
  341. );
  342. },
  343. ),
  344. ),
  345. Visibility(
  346. visible: isCustomEnabled,
  347. replacement: SizedBox(width: 115.w, height: 36.h),
  348. child: GestureDetector(
  349. onTap: onCustomClick,
  350. child: Container(
  351. margin: EdgeInsets.only(top: 3.h),
  352. child: DottedBorder(
  353. color: const Color(0xFFC9C2DB),
  354. strokeWidth: 1.0.w,
  355. borderType: BorderType.RRect,
  356. radius: Radius.circular(20.r),
  357. child: Container(
  358. width: 115.w,
  359. height: 33.h,
  360. alignment: Alignment.center,
  361. child: Row(
  362. mainAxisAlignment: MainAxisAlignment.center,
  363. children: [
  364. Assets.images.iconCharacterCustomPlus.image(
  365. width: 18.w,
  366. height: 18.w,
  367. ),
  368. Text(
  369. StringName.characterCustomCustomizable,
  370. style: TextStyle(
  371. color: const Color(0xFFC9C2DB),
  372. fontSize: 14.sp,
  373. fontWeight: FontWeight.w500,
  374. ),
  375. ),
  376. ],
  377. ),
  378. ),
  379. ),
  380. ),
  381. ),
  382. ),
  383. SizedBox(height: 18.h),
  384. _buildCurrentSelectedLabels(selectedLabels, onSelected),
  385. Container(
  386. margin: EdgeInsets.only(top: 107.h, bottom: 32.h),
  387. child: _buildNextButton(
  388. isEnable: selectedLabels.isNotEmpty,
  389. onTap: nextClick,
  390. ),
  391. ),
  392. ],
  393. );
  394. });
  395. }
  396. Widget _buildCurrentSelectedLabels(
  397. RxList<dynamic> selectedLabels,
  398. Function(dynamic) onSelected,
  399. ) {
  400. return Row(
  401. children: [
  402. Obx(() {
  403. if (selectedLabels.isEmpty) {
  404. return Container(
  405. padding: EdgeInsets.symmetric(horizontal: 11.w, vertical: 8.h),
  406. child: Row(
  407. children: [
  408. Text(
  409. "",
  410. style: TextStyle(
  411. fontSize: 14.sp,
  412. fontWeight: FontWeight.w400,
  413. ),
  414. ),
  415. Assets.images.iconChangeHobbiesUnselect.image(
  416. width: 14.w,
  417. height: 14.w,
  418. ),
  419. ],
  420. ),
  421. );
  422. }
  423. return Expanded(
  424. child: SingleChildScrollView(
  425. scrollDirection: Axis.horizontal,
  426. child: Padding(
  427. padding: EdgeInsets.only(left: 16.w, right: 16.w),
  428. child: Row(
  429. mainAxisAlignment: MainAxisAlignment.center,
  430. children: [
  431. ...selectedLabels.map((item) {
  432. return Container(
  433. margin: EdgeInsets.only(left: 8.w),
  434. child: Obx(() {
  435. final isSelected = selectedLabels.contains(item);
  436. return GestureDetector(
  437. onTap: () => onSelected(item),
  438. child: Container(
  439. padding: EdgeInsets.symmetric(
  440. horizontal: 11.w,
  441. vertical: 8.h,
  442. ),
  443. decoration: BoxDecoration(
  444. gradient:
  445. isSelected
  446. ? LinearGradient(
  447. colors: [
  448. Color(0xFF7D46FC),
  449. Color(0xFFBC87FF),
  450. ],
  451. begin: Alignment.centerLeft,
  452. end: Alignment.centerRight,
  453. )
  454. : null,
  455. color: isSelected ? null : Colors.white,
  456. borderRadius: BorderRadius.circular(70.r),
  457. border: Border.all(
  458. color: Colors.transparent,
  459. width: 0,
  460. ),
  461. ),
  462. child: Row(
  463. children: [
  464. Text(
  465. "${item.emoji ?? ""}${item.name}",
  466. style: TextStyle(
  467. color:
  468. isSelected
  469. ? Colors.white
  470. : Color(0xFF755BAB),
  471. fontSize: 14.sp,
  472. fontWeight: FontWeight.w400,
  473. ),
  474. ),
  475. Assets.images.iconChangeHobbiesUnselect
  476. .image(width: 14.w, height: 14.w),
  477. ],
  478. ),
  479. ),
  480. );
  481. }),
  482. );
  483. }),
  484. ],
  485. ),
  486. ),
  487. ),
  488. );
  489. }),
  490. ],
  491. );
  492. }
  493. Widget _buildNextButton({required VoidCallback onTap, required isEnable}) {
  494. return GestureDetector(
  495. onTap: () {
  496. onTap();
  497. },
  498. child: Container(
  499. width: 220.w,
  500. height: 48.h,
  501. decoration:
  502. isEnable
  503. ? Styles.getActivateButtonDecoration(31.r)
  504. : Styles.getInactiveButtonDecoration(31.r),
  505. child: Center(
  506. child: Text(
  507. '下一步',
  508. style: TextStyle(
  509. color: Colors.white,
  510. fontSize: 16.sp,
  511. fontWeight: FontWeight.w500,
  512. ),
  513. ),
  514. ),
  515. ),
  516. );
  517. }
  518. // 选择爱好页面
  519. Widget _buildHobbiesPage() {
  520. return _buildSelectionPage(
  521. title: StringName.characterCustomHobbiesTitle,
  522. subtitle:
  523. "(最多选择${controller.currentCharacterCustomConfig?.maxHobbyNum ?? 3}个)",
  524. items: controller.hobbiesLabelsList,
  525. selectedLabels: controller.hobbiesSelectLabels,
  526. isShowEmoji: true,
  527. onSelected: (name) {
  528. controller.selectHobby(name);
  529. },
  530. isCustomEnabled:
  531. true,
  532. onCustomClick: () {
  533. controller.clickHobbiesCustom();
  534. },
  535. nextClick: () {
  536. controller.clickHobbiesNext();
  537. },
  538. );
  539. }
  540. // 选择性格页面
  541. Widget _buildCharacterPage() {
  542. return _buildSelectionPage(
  543. title: StringName.characterCustomcharacterTitle,
  544. subtitle:
  545. "(最多选择${controller.currentCharacterCustomConfig?.maxCharacterNum ?? 3}个)",
  546. items: controller.characterLabelsList,
  547. selectedLabels: controller.characterSelectLabels,
  548. isShowEmoji: true,
  549. onSelected: (character) {
  550. controller.selectCharacter(character);
  551. },
  552. isCustomEnabled:
  553. controller.currentCharacterCustomConfig?.customCharacter == true,
  554. onCustomClick: () {
  555. controller.clickCharacterCustom();
  556. },
  557. nextClick: () {
  558. controller.clickCharacterNext();
  559. },
  560. );
  561. }
  562. // 输入名字页面
  563. Widget _buildInputNamePage() {
  564. return Obx(() {
  565. return Column(
  566. children: [
  567. Padding(
  568. padding: EdgeInsets.only(top: 15.h),
  569. child: Text(
  570. StringName.characterCustomNameTitle,
  571. style: TextStyle(
  572. color: const Color(0xFF755BAB),
  573. fontSize: 18.sp,
  574. fontWeight: FontWeight.w500,
  575. ),
  576. ),
  577. ),
  578. Text(
  579. "人设名称(最多5个字)",
  580. style: TextStyle(
  581. color: Color(0xFF755BAB).withValues(alpha: 0.6),
  582. fontSize: 12.sp,
  583. fontWeight: FontWeight.w500,
  584. ),
  585. ),
  586. Container(
  587. margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
  588. alignment: Alignment.center,
  589. child: Container(
  590. height: 48.h,
  591. alignment: Alignment.center,
  592. decoration: ShapeDecoration(
  593. color: const Color(0xFFF5F4F9),
  594. shape: RoundedRectangleBorder(
  595. borderRadius: BorderRadius.circular(31.r),
  596. ),
  597. ),
  598. child: TextField(
  599. // maxLength: maxLength,
  600. maxLines: 1,
  601. // expands: true,
  602. textAlign: TextAlign.center,
  603. textAlignVertical: TextAlignVertical.center,
  604. onChanged: (value) {
  605. controller.currentNameValue.value = value;
  606. },
  607. decoration: InputDecoration(
  608. isDense: true,
  609. counterText: "",
  610. hintText: StringName.characterCustomNameHint,
  611. hintStyle: TextStyle(color: Colors.black.withAlpha(66)),
  612. border: OutlineInputBorder(
  613. borderRadius: BorderRadius.circular(31.r),
  614. borderSide: BorderSide.none, // 移除边框线
  615. ),
  616. filled: true,
  617. fillColor: const Color(0xFFF5F4F9),
  618. ),
  619. ),
  620. ),
  621. ),
  622. Container(
  623. margin: EdgeInsets.only(top: 44.h, bottom: 32.h),
  624. child: _buildNextButton(
  625. isEnable: controller.currentNameValue.trim().isNotEmpty,
  626. onTap: () {
  627. controller.clickInputNameNext();
  628. },
  629. ),
  630. ),
  631. ],
  632. );
  633. });
  634. }
  635. }