character_custom_page.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  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: 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. SizedBox(height: 18.h),
  399. _buildCurrentSelectedLabels(selectedLabels, onSelected),
  400. Container(
  401. margin: EdgeInsets.only(top: 107.h, bottom: 32.h),
  402. child: _buildNextButton(
  403. isEnable: selectedLabels.isNotEmpty,
  404. onTap: nextClick,
  405. ),
  406. ),
  407. ],
  408. );
  409. });
  410. }
  411. Widget _buildCurrentSelectedLabels(
  412. RxList<dynamic> selectedLabels,
  413. Function(dynamic) onSelected,
  414. ) {
  415. return Row(
  416. children: [
  417. Obx(() {
  418. if (selectedLabels.isEmpty) {
  419. return Container(
  420. padding: EdgeInsets.symmetric(horizontal: 11.w, vertical: 8.h),
  421. child: Row(
  422. children: [
  423. Text(
  424. "",
  425. style: TextStyle(
  426. fontSize: 14.sp,
  427. fontWeight: FontWeight.w400,
  428. ),
  429. ),
  430. Assets.images.iconChangeHobbiesUnselect.image(
  431. width: 14.w,
  432. height: 14.w,
  433. ),
  434. ],
  435. ),
  436. );
  437. }
  438. return Expanded(
  439. child: SingleChildScrollView(
  440. scrollDirection: Axis.horizontal,
  441. child: Padding(
  442. padding: EdgeInsets.only(left: 16.w, right: 16.w),
  443. child: Row(
  444. mainAxisAlignment: MainAxisAlignment.center,
  445. children: [
  446. ...selectedLabels.map((item) {
  447. return Container(
  448. margin: EdgeInsets.only(left: 8.w),
  449. child: Obx(() {
  450. final isSelected = selectedLabels.contains(item);
  451. return GestureDetector(
  452. onTap: () => onSelected(item),
  453. child: Container(
  454. padding: EdgeInsets.symmetric(
  455. horizontal: 11.w,
  456. vertical: 8.h,
  457. ),
  458. decoration: BoxDecoration(
  459. gradient:
  460. isSelected
  461. ? LinearGradient(
  462. colors: [
  463. Color(0xFF7D46FC),
  464. Color(0xFFBC87FF),
  465. ],
  466. begin: Alignment.centerLeft,
  467. end: Alignment.centerRight,
  468. )
  469. : null,
  470. color: isSelected ? null : Colors.white,
  471. borderRadius: BorderRadius.circular(70.r),
  472. border: Border.all(
  473. color: Colors.transparent,
  474. width: 0,
  475. ),
  476. ),
  477. child: Row(
  478. children: [
  479. Text(
  480. "${item.emoji ?? ""}${item.name}",
  481. style: TextStyle(
  482. color:
  483. isSelected
  484. ? Colors.white
  485. : Color(0xFF755BAB),
  486. fontSize: 14.sp,
  487. fontWeight: FontWeight.w400,
  488. ),
  489. ),
  490. Assets.images.iconChangeHobbiesUnselect
  491. .image(width: 14.w, height: 14.w),
  492. ],
  493. ),
  494. ),
  495. );
  496. }),
  497. );
  498. }),
  499. ],
  500. ),
  501. ),
  502. ),
  503. );
  504. }),
  505. ],
  506. );
  507. }
  508. Widget _buildNextButton({required VoidCallback onTap, required isEnable}) {
  509. return GestureDetector(
  510. onTap: () {
  511. onTap();
  512. },
  513. child: Container(
  514. width: 220.w,
  515. height: 48.h,
  516. decoration:
  517. isEnable
  518. ? Styles.getActivateButtonDecoration(31.r)
  519. : Styles.getInactiveButtonDecoration(31.r),
  520. child: Center(
  521. child: Text(
  522. '下一步',
  523. style: TextStyle(
  524. color: Colors.white,
  525. fontSize: 16.sp,
  526. fontWeight: FontWeight.w500,
  527. ),
  528. ),
  529. ),
  530. ),
  531. );
  532. }
  533. // 选择爱好页面
  534. Widget _buildHobbiesPage() {
  535. return _buildSelectionPage(
  536. title: StringName.characterCustomHobbiesTitle,
  537. subtitle:
  538. "(最多选择${controller.currentCharacterCustomConfig?.maxHobbyNum ?? 3}个)",
  539. items: controller.hobbiesLabelsList,
  540. selectedLabels: controller.hobbiesSelectLabels,
  541. isShowEmoji: true,
  542. onSelected: (name) {
  543. controller.selectHobby(name);
  544. },
  545. isCustomEnabled:
  546. true,
  547. onCustomClick: () {
  548. controller.clickHobbiesCustom();
  549. },
  550. nextClick: () {
  551. controller.clickHobbiesNext();
  552. },
  553. );
  554. }
  555. // 选择性格页面
  556. Widget _buildCharacterPage() {
  557. return _buildSelectionPage(
  558. title: StringName.characterCustomcharacterTitle,
  559. subtitle:
  560. "(最多选择${controller.currentCharacterCustomConfig?.maxCharacterNum ?? 3}个)",
  561. items: controller.characterLabelsList,
  562. selectedLabels: controller.characterSelectLabels,
  563. isShowEmoji: true,
  564. onSelected: (character) {
  565. controller.selectCharacter(character);
  566. },
  567. isCustomEnabled:
  568. controller.currentCharacterCustomConfig?.customCharacter == true,
  569. onCustomClick: () {
  570. controller.clickCharacterCustom();
  571. },
  572. nextClick: () {
  573. controller.clickCharacterNext();
  574. },
  575. );
  576. }
  577. // 输入名字页面
  578. Widget _buildInputNamePage() {
  579. return Obx(() {
  580. return Column(
  581. children: [
  582. Padding(
  583. padding: EdgeInsets.only(top: 15.h),
  584. child: Text(
  585. StringName.characterCustomNameTitle,
  586. style: TextStyle(
  587. color: const Color(0xFF755BAB),
  588. fontSize: 18.sp,
  589. fontWeight: FontWeight.w500,
  590. ),
  591. ),
  592. ),
  593. Text(
  594. "人设名称(最多5个字)",
  595. style: TextStyle(
  596. color: Color(0xFF755BAB).withValues(alpha: 0.6),
  597. fontSize: 12.sp,
  598. fontWeight: FontWeight.w500,
  599. ),
  600. ),
  601. Container(
  602. margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
  603. alignment: Alignment.center,
  604. child: Container(
  605. height: 48.h,
  606. alignment: Alignment.center,
  607. decoration: ShapeDecoration(
  608. color: const Color(0xFFF5F4F9),
  609. shape: RoundedRectangleBorder(
  610. borderRadius: BorderRadius.circular(31.r),
  611. ),
  612. ),
  613. child: TextField(
  614. // maxLength: maxLength,
  615. maxLines: 1,
  616. // expands: true,
  617. textAlign: TextAlign.center,
  618. textAlignVertical: TextAlignVertical.center,
  619. onChanged: (value) {
  620. controller.currentNameValue.value = value;
  621. },
  622. decoration: InputDecoration(
  623. isDense: true,
  624. counterText: "",
  625. hintText: StringName.characterCustomNameHint,
  626. hintStyle: TextStyle(color: Colors.black.withAlpha(66)),
  627. border: OutlineInputBorder(
  628. borderRadius: BorderRadius.circular(31.r),
  629. borderSide: BorderSide.none, // 移除边框线
  630. ),
  631. filled: true,
  632. fillColor: const Color(0xFFF5F4F9),
  633. ),
  634. ),
  635. ),
  636. ),
  637. Container(
  638. margin: EdgeInsets.only(top: 44.h, bottom: 32.h),
  639. child: _buildNextButton(
  640. isEnable: controller.currentNameValue.trim().isNotEmpty,
  641. onTap: () {
  642. controller.clickInputNameNext();
  643. },
  644. ),
  645. ),
  646. ],
  647. );
  648. });
  649. }
  650. }