character_custom_page.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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),
  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: Column(
  151. mainAxisAlignment: MainAxisAlignment.center,
  152. crossAxisAlignment: CrossAxisAlignment.center,
  153. children: [
  154. controller.currentStep.value == StepType.hobbies
  155. ? Assets.images.iconCharacterCustomStepOneTitle.image(
  156. fit: BoxFit.cover,
  157. width: 209.w,
  158. )
  159. : controller.currentStep.value == StepType.characters
  160. ? Assets.images.iconCharacterCustomStepTwoTitle.image(
  161. fit: BoxFit.cover,
  162. width: 168.w,
  163. )
  164. : Assets.images.iconCharacterCustomStepThreeTitle
  165. .image(fit: BoxFit.cover, width: 207.w),
  166. Container(
  167. margin: EdgeInsets.only(
  168. left: 16.w,
  169. right: 16.w,
  170. top: 24.h,
  171. ),
  172. width: double.infinity,
  173. decoration: ShapeDecoration(
  174. color: Colors.white,
  175. shape: RoundedRectangleBorder(
  176. borderRadius: BorderRadius.circular(20.r),
  177. ),
  178. shadows: [
  179. BoxShadow(
  180. color: Color(0x66D788FF),
  181. blurRadius: 10.r,
  182. offset: Offset(0, 0),
  183. spreadRadius: 0,
  184. ),
  185. ],
  186. ),
  187. child: Column(
  188. children: [
  189. Container(
  190. padding: EdgeInsets.symmetric(horizontal: 26.w),
  191. decoration: BoxDecoration(
  192. image: DecorationImage(
  193. image:
  194. Assets.images.bgCharacterCustomStepsDesc
  195. .provider(),
  196. fit: BoxFit.fill,
  197. ),
  198. ),
  199. child: Text(
  200. StringName.characterCustomStepsDesc,
  201. style: TextStyle(
  202. color: const Color(0xFFAD88EB),
  203. fontSize: 12.sp,
  204. fontWeight: FontWeight.w400,
  205. ),
  206. ),
  207. ),
  208. SizedBox(height: 24.h),
  209. Row(
  210. mainAxisAlignment: MainAxisAlignment.center,
  211. crossAxisAlignment: CrossAxisAlignment.center,
  212. children: [
  213. Text(
  214. '第${controller.currentStep.value.value}步',
  215. style: TextStyle(
  216. color: const Color(0xFF755AAB),
  217. fontSize: 12.sp,
  218. fontWeight: FontWeight.w500,
  219. ),
  220. ),
  221. Text(
  222. " | ",
  223. style: TextStyle(
  224. color: const Color(0xFF755AAB),
  225. fontSize: 12.sp,
  226. fontWeight: FontWeight.w500,
  227. ),
  228. ),
  229. Opacity(
  230. opacity: 0.60,
  231. child: Text(
  232. '共3步',
  233. style: TextStyle(
  234. color: const Color(0xFF755BAB),
  235. fontSize: 12.sp,
  236. fontWeight: FontWeight.w500,
  237. ),
  238. ),
  239. ),
  240. ],
  241. ),
  242. if (controller.currentStep.value ==
  243. StepType.hobbies)
  244. _buildHobbiesPage(),
  245. if (controller.currentStep.value ==
  246. StepType.characters)
  247. _buildCharacterPage(),
  248. if (controller.currentStep.value ==
  249. StepType.inputName)
  250. _buildInputNamePage(),
  251. ],
  252. ),
  253. ),
  254. ],
  255. ),
  256. ),
  257. ],
  258. ),
  259. ),
  260. ],
  261. );
  262. }
  263. Widget _buildSelectionPage({
  264. required String title,
  265. required String subtitle,
  266. required List<dynamic> items,
  267. required RxList<dynamic> selectedLabels,
  268. required Function(dynamic) onSelected,
  269. required bool isCustomEnabled,
  270. required VoidCallback onCustomClick,
  271. required VoidCallback nextClick,
  272. required bool isShowEmoji,
  273. }) {
  274. return Obx(() {
  275. int rowCount = 3; // 3 行
  276. int columnCount = (items.length / rowCount).ceil(); // 计算列数
  277. return Column(
  278. children: [
  279. Padding(
  280. padding: EdgeInsets.only(top: 15.h),
  281. child: Text(
  282. title,
  283. style: TextStyle(
  284. color: const Color(0xFF755BAB),
  285. fontSize: 18.sp,
  286. fontWeight: FontWeight.w500,
  287. ),
  288. ),
  289. ),
  290. Text(
  291. subtitle,
  292. style: TextStyle(
  293. color: const Color(0xFF755BAB).withOpacity(0.6),
  294. fontSize: 12.sp,
  295. fontWeight: FontWeight.w500,
  296. ),
  297. ),
  298. Container(
  299. margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
  300. height: 180.h,
  301. child: StaggeredAutoScrollListView(
  302. itemCount: items.length,
  303. isAutoScrolling: true,
  304. itemBuilder: (context, index) {
  305. final item = items[index];
  306. final emoji = item.emoji ?? "";
  307. final name = item.name ?? "";
  308. return Padding(
  309. padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 4.w),
  310. child: Obx(() {
  311. bool isSelected = selectedLabels.any(
  312. (selected) => selected.name == item.name,
  313. );
  314. return ChoiceChip(
  315. label: Text(
  316. isShowEmoji ? "$emoji$name" : name,
  317. style: TextStyle(
  318. color: isSelected ? Colors.white : const Color(0xFF755BAB),
  319. fontSize: 14.sp,
  320. fontWeight: FontWeight.w400,
  321. ),
  322. ),
  323. showCheckmark: false,
  324. selected: isSelected,
  325. selectedColor: const Color(0xFFB782FF),
  326. backgroundColor: Colors.white,
  327. shape: RoundedRectangleBorder(
  328. side: BorderSide(
  329. width: 1.w,
  330. color: const Color(0x4C755BAB),
  331. ),
  332. borderRadius: BorderRadius.circular(31.r),
  333. ),
  334. onSelected: (selected) {
  335. onSelected(item);
  336. },
  337. );
  338. }),
  339. );
  340. },
  341. ),
  342. ),
  343. Visibility(
  344. visible: isCustomEnabled,
  345. replacement: SizedBox(width: 115.w, height: 36.h),
  346. child: GestureDetector(
  347. onTap: onCustomClick,
  348. child: Container(
  349. margin: EdgeInsets.only(top: 3.h),
  350. child: DottedBorder(
  351. color: const Color(0xFFC9C2DB),
  352. strokeWidth: 1.0.w,
  353. borderType: BorderType.RRect,
  354. radius: Radius.circular(20.r),
  355. child: Container(
  356. width: 115.w,
  357. height: 33.h,
  358. alignment: Alignment.center,
  359. child: Row(
  360. mainAxisAlignment: MainAxisAlignment.center,
  361. children: [
  362. Assets.images.iconCharacterCustomPlus.image(
  363. width: 18.w,
  364. height: 18.w,
  365. ),
  366. Text(
  367. StringName.characterCustomCustomizable,
  368. style: TextStyle(
  369. color: const Color(0xFFC9C2DB),
  370. fontSize: 14.sp,
  371. fontWeight: FontWeight.w500,
  372. ),
  373. ),
  374. ],
  375. ),
  376. ),
  377. ),
  378. ),
  379. ),
  380. ),
  381. SizedBox(height: 18.h),
  382. _buildCurrentSelectedLabels(selectedLabels, onSelected),
  383. Container(
  384. margin: EdgeInsets.only(top: 44.h, bottom: 32.h),
  385. child: _buildNextButton(
  386. isEnable: selectedLabels.isNotEmpty,
  387. onTap: nextClick,
  388. ),
  389. ),
  390. ],
  391. );
  392. });
  393. }
  394. Widget _buildCurrentSelectedLabels(
  395. RxList<dynamic> selectedLabels,
  396. Function(dynamic) onSelected,
  397. ) {
  398. return Row(
  399. children: [
  400. Obx(() {
  401. if (selectedLabels.isEmpty) {
  402. return Container(
  403. padding: EdgeInsets.symmetric(horizontal: 11.w, vertical: 8.h),
  404. child: Row(
  405. children: [
  406. Text(
  407. "",
  408. style: TextStyle(
  409. fontSize: 14.sp,
  410. fontWeight: FontWeight.w400,
  411. ),
  412. ),
  413. Assets.images.iconChangeHobbiesUnselect.image(
  414. width: 14.w,
  415. height: 14.w,
  416. ),
  417. ],
  418. ),
  419. );
  420. }
  421. return Expanded(
  422. child: SingleChildScrollView(
  423. scrollDirection: Axis.horizontal,
  424. child: Padding(
  425. padding: EdgeInsets.only(left: 16.w, right: 16.w),
  426. child: Row(
  427. mainAxisAlignment: MainAxisAlignment.center,
  428. children: [
  429. ...selectedLabels.map((item) {
  430. return Container(
  431. margin: EdgeInsets.only(left: 8.w),
  432. child: Obx(() {
  433. final isSelected = selectedLabels.contains(item);
  434. return GestureDetector(
  435. onTap: () => onSelected(item),
  436. child: Container(
  437. padding: EdgeInsets.symmetric(
  438. horizontal: 11.w,
  439. vertical: 8.h,
  440. ),
  441. decoration: BoxDecoration(
  442. gradient:
  443. isSelected
  444. ? LinearGradient(
  445. colors: [
  446. Color(0xFF7D46FC),
  447. Color(0xFFBC87FF),
  448. ],
  449. begin: Alignment.centerLeft,
  450. end: Alignment.centerRight,
  451. )
  452. : null,
  453. color: isSelected ? null : Colors.white,
  454. borderRadius: BorderRadius.circular(70.r),
  455. border: Border.all(
  456. color: Colors.transparent,
  457. width: 0,
  458. ),
  459. ),
  460. child: Row(
  461. children: [
  462. Text(
  463. "${item.emoji ?? ""}${item.name}",
  464. style: TextStyle(
  465. color:
  466. isSelected
  467. ? Colors.white
  468. : Color(0xFF755BAB),
  469. fontSize: 14.sp,
  470. fontWeight: FontWeight.w400,
  471. ),
  472. ),
  473. Assets.images.iconChangeHobbiesUnselect
  474. .image(width: 14.w, height: 14.w),
  475. ],
  476. ),
  477. ),
  478. );
  479. }),
  480. );
  481. }),
  482. ],
  483. ),
  484. ),
  485. ),
  486. );
  487. }),
  488. ],
  489. );
  490. }
  491. Widget _buildNextButton({required VoidCallback onTap, required isEnable}) {
  492. return GestureDetector(
  493. onTap: () {
  494. onTap();
  495. },
  496. child: Container(
  497. width: 220.w,
  498. height: 48.h,
  499. decoration:
  500. isEnable
  501. ? Styles.getActivateButtonDecoration(31.r)
  502. : Styles.getInactiveButtonDecoration(31.r),
  503. child: Center(
  504. child: Text(
  505. '下一步',
  506. style: TextStyle(
  507. color: Colors.white,
  508. fontSize: 16.sp,
  509. fontWeight: FontWeight.w500,
  510. ),
  511. ),
  512. ),
  513. ),
  514. );
  515. }
  516. // 选择爱好页面
  517. Widget _buildHobbiesPage() {
  518. return _buildSelectionPage(
  519. title: StringName.characterCustomHobbiesTitle,
  520. subtitle:
  521. "(最多选择${controller.currentCharacterCustomConfig?.maxHobbyNum ?? 3}个)",
  522. items: controller.hobbiesLabelsList,
  523. selectedLabels: controller.hobbiesSelectLabels,
  524. isShowEmoji: true,
  525. onSelected: (name) {
  526. controller.selectHobby(name);
  527. },
  528. isCustomEnabled:
  529. true,
  530. onCustomClick: () {
  531. controller.clickHobbiesCustom();
  532. },
  533. nextClick: () {
  534. controller.clickHobbiesNext();
  535. },
  536. );
  537. }
  538. // 选择性格页面
  539. Widget _buildCharacterPage() {
  540. return _buildSelectionPage(
  541. title: StringName.characterCustomcharacterTitle,
  542. subtitle:
  543. "(最多选择${controller.currentCharacterCustomConfig?.maxCharacterNum ?? 3}个)",
  544. items: controller.characterLabelsList,
  545. selectedLabels: controller.characterSelectLabels,
  546. isShowEmoji: true,
  547. onSelected: (character) {
  548. controller.selectCharacter(character);
  549. },
  550. isCustomEnabled:
  551. controller.currentCharacterCustomConfig?.customCharacter == true,
  552. onCustomClick: () {
  553. controller.clickCharacterCustom();
  554. },
  555. nextClick: () {
  556. controller.clickCharacterNext();
  557. },
  558. );
  559. }
  560. // 输入名字页面
  561. Widget _buildInputNamePage() {
  562. return Obx(() {
  563. return Column(
  564. children: [
  565. Padding(
  566. padding: EdgeInsets.only(top: 15.h),
  567. child: Text(
  568. StringName.characterCustomNameTitle,
  569. style: TextStyle(
  570. color: const Color(0xFF755BAB),
  571. fontSize: 18.sp,
  572. fontWeight: FontWeight.w500,
  573. ),
  574. ),
  575. ),
  576. Text(
  577. "人设名称(最多5个字)",
  578. style: TextStyle(
  579. color: Color(0xFF755BAB).withValues(alpha: 0.6),
  580. fontSize: 12.sp,
  581. fontWeight: FontWeight.w500,
  582. ),
  583. ),
  584. Container(
  585. margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
  586. alignment: Alignment.center,
  587. child: Container(
  588. height: 48.h,
  589. alignment: Alignment.center,
  590. decoration: ShapeDecoration(
  591. color: const Color(0xFFF5F4F9),
  592. shape: RoundedRectangleBorder(
  593. borderRadius: BorderRadius.circular(31.r),
  594. ),
  595. ),
  596. child: TextField(
  597. // maxLength: maxLength,
  598. maxLines: 1,
  599. // expands: true,
  600. textAlign: TextAlign.center,
  601. textAlignVertical: TextAlignVertical.center,
  602. onChanged: (value) {
  603. controller.currentNameValue.value = value;
  604. },
  605. decoration: InputDecoration(
  606. isDense: true,
  607. counterText: "",
  608. hintText: StringName.characterCustomNameHint,
  609. hintStyle: TextStyle(color: Colors.black.withAlpha(66)),
  610. border: OutlineInputBorder(
  611. borderRadius: BorderRadius.circular(31.r),
  612. borderSide: BorderSide.none, // 移除边框线
  613. ),
  614. filled: true,
  615. fillColor: const Color(0xFFF5F4F9),
  616. ),
  617. ),
  618. ),
  619. ),
  620. Container(
  621. margin: EdgeInsets.only(top: 44.h, bottom: 32.h),
  622. child: _buildNextButton(
  623. isEnable: controller.currentNameValue.trim().isNotEmpty,
  624. onTap: () {
  625. controller.clickInputNameNext();
  626. },
  627. ),
  628. ),
  629. ],
  630. );
  631. });
  632. }
  633. }