character_view.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import 'package:keyboard/base/base_view.dart';
  5. import 'package:keyboard/resource/string.gen.dart';
  6. import '../../resource/assets.gen.dart';
  7. import 'character_controller.dart';
  8. class CharacterView extends BaseView<CharacterController> {
  9. const CharacterView({super.key});
  10. @override
  11. backgroundColor() {
  12. return Colors.transparent;
  13. }
  14. @override
  15. Widget buildBody(BuildContext context) {
  16. return Scaffold(
  17. backgroundColor: Color(0xFFF6F5FA),
  18. body: Builder(
  19. builder: (context) {
  20. return NestedScrollView(
  21. headerSliverBuilder: (context, innerBoxIsScrolled) {
  22. return [
  23. /// **🔹 让背景图滑动时裁剪掉上方部分**
  24. SliverPersistentHeader(
  25. pinned: true,
  26. delegate: CharacterHeaderDelegate(
  27. expandedHeight: 240.h,
  28. minHeight: 100.h,
  29. // bottomWidget: SizedBox(),
  30. onTap: controller.clickMyKeyboard,
  31. ),
  32. ),
  33. SliverPersistentHeader(
  34. pinned: true,
  35. // floating: true,
  36. delegate: TabBarDelegate(
  37. height: 180.h,
  38. child: _bottomAppBar(),
  39. ),
  40. ),
  41. ];
  42. },
  43. body: _pages(),
  44. );
  45. },
  46. ),
  47. );
  48. }
  49. /// **自定义 bottomAppBar**
  50. Widget _bottomAppBar() {
  51. return Container(
  52. decoration: ShapeDecoration(
  53. gradient: LinearGradient(
  54. begin: Alignment(0.50, -0.00),
  55. end: Alignment(0.50, 1.00),
  56. colors: [Color(0xFFEAE5FF), Color(0xFFF5F4F9)],
  57. ),
  58. shape: RoundedRectangleBorder(
  59. borderRadius: BorderRadius.only(
  60. topLeft: Radius.circular(20.r),
  61. topRight: Radius.circular(20.r),
  62. ),
  63. ),
  64. ),
  65. child: Column(
  66. mainAxisAlignment: MainAxisAlignment.center,
  67. crossAxisAlignment: CrossAxisAlignment.start,
  68. mainAxisSize: MainAxisSize.min,
  69. children: [
  70. _customizeButton(),
  71. SizedBox(height: 14.h),
  72. Row(
  73. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  74. children: [
  75. Assets.images.iconCharacterMarket.image(
  76. width: 73.w,
  77. height: 25.h,
  78. ),
  79. Obx(() {
  80. return DropdownButton<String>(
  81. // hint: Text(''),
  82. underline: Container(height: 0),
  83. style: TextStyle(
  84. color: Colors.black.withAlpha(102),
  85. fontSize: 14.sp,
  86. fontWeight: FontWeight.w400,
  87. ),
  88. icon: Assets.images.iconCharacterArrowDown.image(
  89. width: 20.r,
  90. height: 20.r,
  91. ),
  92. value: controller.selectedValue.value,
  93. // 绑定 selectedValue
  94. onChanged: (String? newValue) {
  95. controller.updateSelectedValue(newValue); // 更新选中的值
  96. },
  97. items: List.generate(controller.keyboardOptions.length, (
  98. index,
  99. ) {
  100. String value = controller.keyboardOptions[index];
  101. return DropdownMenuItem<String>(
  102. value: value,
  103. child: Column(
  104. crossAxisAlignment: CrossAxisAlignment.start,
  105. // 让选项文本左对齐
  106. mainAxisSize: MainAxisSize.min,
  107. // 让 Column 适配内容
  108. children: [
  109. Padding(
  110. padding: EdgeInsets.symmetric(vertical: 8),
  111. child: Text(
  112. value,
  113. style: TextStyle(
  114. color: Colors.black.withAlpha(204),
  115. fontSize: 14.sp,
  116. fontWeight: FontWeight.w400,
  117. ),
  118. ),
  119. ),
  120. if (index != controller.keyboardOptions.length - 1)
  121. Divider(
  122. color: Color(0xFFF6F6F6),
  123. thickness: 1,
  124. height: 1,
  125. ),
  126. ],
  127. ),
  128. );
  129. }),
  130. );
  131. }),
  132. ],
  133. ),
  134. SizedBox(height: 15.h),
  135. tabBar(),
  136. ],
  137. ),
  138. );
  139. }
  140. // 定制按钮
  141. Widget _customizeButton() {
  142. return Container(
  143. margin: EdgeInsets.only(left: 16.w),
  144. width: 220.w,
  145. height: 56.h,
  146. padding: EdgeInsets.symmetric(horizontal: 10.w),
  147. decoration: ShapeDecoration(
  148. color: const Color(0xFF121212),
  149. shape: RoundedRectangleBorder(
  150. borderRadius: BorderRadius.circular(40.r),
  151. ),
  152. ),
  153. child: Row(
  154. mainAxisAlignment: MainAxisAlignment.start,
  155. children: [
  156. Assets.images.iconCharacterCustomized.image(
  157. width: 36.r,
  158. height: 36.r,
  159. ),
  160. SizedBox(width: 8.w),
  161. Column(
  162. crossAxisAlignment: CrossAxisAlignment.start,
  163. mainAxisAlignment: MainAxisAlignment.center,
  164. children: [
  165. Text(
  166. StringName.goCustomizeCharacter,
  167. textAlign: TextAlign.center,
  168. style: TextStyle(
  169. color: Colors.white,
  170. fontSize: 16.sp,
  171. fontWeight: FontWeight.w500,
  172. ),
  173. ),
  174. Text(
  175. StringName.goCustomizeCharacterDesc,
  176. style: TextStyle(
  177. color: Color(0xFFF5F4F9),
  178. fontSize: 11.sp,
  179. fontWeight: FontWeight.w400,
  180. ),
  181. ),
  182. ],
  183. ),
  184. Container(
  185. margin: EdgeInsets.only(left: 16.w),
  186. width: 24.r,
  187. height: 24.r,
  188. decoration: ShapeDecoration(
  189. color: Colors.white,
  190. shape: OvalBorder(),
  191. ),
  192. child: Assets.images.iconCharacterArrowRight.image(
  193. width: 16.r,
  194. height: 16.r,
  195. ),
  196. ),
  197. ],
  198. ),
  199. );
  200. }
  201. /// **TabBar**
  202. Widget tabBar() {
  203. return Obx(() {
  204. if (controller.characterGroupList.isEmpty) {
  205. return const SizedBox.shrink();
  206. }
  207. return TabBar(
  208. controller: controller.tabController.value,
  209. dividerHeight: 0,
  210. tabAlignment: TabAlignment.start,
  211. isScrollable: true,
  212. padding: EdgeInsets.symmetric(horizontal: 12.w),
  213. labelPadding: EdgeInsets.symmetric(horizontal: 4.w),
  214. indicator: const BoxDecoration(),
  215. onTap: (index) => controller.onTabChanged(index),
  216. tabs: List.generate(controller.characterGroupList.length, (index) {
  217. var e = controller.characterGroupList[index];
  218. bool isSelected = index == controller.currentTabBarIndex.value;
  219. return Container(
  220. width: 80.w,
  221. height: isSelected ? 38.h : 32.h,
  222. decoration:
  223. isSelected
  224. ? BoxDecoration(
  225. borderRadius: BorderRadius.circular(36.r),
  226. image: DecorationImage(
  227. image:
  228. Assets.images.iconCharacterGroupSelected.provider(),
  229. fit: BoxFit.cover,
  230. ),
  231. )
  232. : BoxDecoration(
  233. color: Colors.white.withAlpha(204),
  234. borderRadius: BorderRadius.circular(36.r),
  235. ),
  236. child: Row(
  237. mainAxisAlignment: MainAxisAlignment.center,
  238. children: [
  239. if (e.iconUrl != null)
  240. Image.network(e.iconUrl!, width: 20.r, height: 20.r),
  241. Text(
  242. e.name,
  243. style: TextStyle(
  244. color:
  245. isSelected ? Colors.black : Colors.black.withAlpha(104),
  246. fontSize: 14.sp,
  247. fontWeight: FontWeight.w500,
  248. ),
  249. ),
  250. ],
  251. ),
  252. );
  253. }),
  254. );
  255. });
  256. }
  257. Widget _pages() {
  258. return Obx(() {
  259. if (controller.characterGroupList.isEmpty) {
  260. return const Center(child: CircularProgressIndicator());
  261. }
  262. return PageView(
  263. controller: controller.pageController,
  264. onPageChanged: (index) {
  265. controller.onPageChanged(index);
  266. },
  267. children:
  268. controller.characterGroupList.map((group) {
  269. return ListView.builder(
  270. itemCount: controller.characterGroupList.length,
  271. itemBuilder: (context, index) {
  272. return ListTile(title: Text(group.name));
  273. },
  274. );
  275. }).toList(),
  276. );
  277. });
  278. }
  279. }
  280. /// **🔹 让背景图滑动时裁剪掉上方部分**
  281. class CharacterHeaderDelegate extends SliverPersistentHeaderDelegate {
  282. final double expandedHeight;
  283. final double minHeight;
  284. // final Widget bottomWidget;
  285. final VoidCallback onTap;
  286. CharacterHeaderDelegate({
  287. required this.expandedHeight,
  288. required this.minHeight,
  289. // required this.bottomWidget,
  290. required this.onTap,
  291. });
  292. @override
  293. Widget build(
  294. BuildContext context,
  295. double shrinkOffset,
  296. bool overlapsContent,
  297. ) {
  298. final currentVisibleHeight = (expandedHeight - shrinkOffset).clamp(
  299. minHeight,
  300. expandedHeight,
  301. );
  302. final tabBarOffset = expandedHeight - currentVisibleHeight; // 计算 TabBar 位移
  303. final opacity = 1 - currentVisibleHeight / expandedHeight;
  304. return Stack(
  305. clipBehavior: Clip.none,
  306. children: [
  307. // 背景图片,动态裁剪
  308. Positioned(
  309. top: 0,
  310. left: 0,
  311. right: 0,
  312. height: currentVisibleHeight,
  313. child: ClipRect(
  314. child: Image.asset(
  315. Assets.images.bgCharacterBoyBanner.path,
  316. fit: BoxFit.cover,
  317. height: expandedHeight,
  318. alignment: Alignment.topCenter,
  319. ),
  320. ),
  321. ),
  322. // 遮罩层 Positioned(用于控制背景的可见性)
  323. Positioned(
  324. top: 0,
  325. left: 0,
  326. right: 0,
  327. height: currentVisibleHeight,
  328. child: Container(color: Colors.black.withValues(alpha: opacity)),
  329. ),
  330. Positioned(
  331. top: 0,
  332. child: SafeArea(
  333. child: GestureDetector(
  334. onTap: onTap,
  335. child: Container(
  336. margin: EdgeInsets.symmetric(horizontal: 16.w),
  337. width: 96.w,
  338. height: 32.h,
  339. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
  340. decoration: ShapeDecoration(
  341. color: Colors.white.withValues(alpha: 153),
  342. shape: RoundedRectangleBorder(
  343. borderRadius: BorderRadius.circular(10),
  344. ),
  345. ),
  346. child: Row(
  347. mainAxisAlignment: MainAxisAlignment.start,
  348. crossAxisAlignment: CrossAxisAlignment.center,
  349. spacing: 4.r,
  350. children: [
  351. Container(
  352. width: 24.r,
  353. height: 24.r,
  354. clipBehavior: Clip.antiAlias,
  355. decoration: BoxDecoration(),
  356. child: Assets.images.iconCharacterKeyboard.image(
  357. width: 24.r,
  358. height: 24.r,
  359. ),
  360. ),
  361. Text(
  362. StringName.myKeyboard,
  363. textAlign: TextAlign.center,
  364. style: TextStyle(
  365. color: Colors.black.withAlpha(204),
  366. fontSize: 14.sp,
  367. fontWeight: FontWeight.w400,
  368. ),
  369. ),
  370. ],
  371. ),
  372. ),
  373. ),
  374. ),
  375. ),
  376. // TabBar 定位
  377. // Positioned(
  378. // bottom: tabBarOffset,
  379. // left: 0,
  380. // right: 0,
  381. // child: Transform.translate(
  382. // offset: Offset(0, tabBarOffset),
  383. // child: bottomWidget,
  384. // ),
  385. // ),
  386. ],
  387. );
  388. }
  389. @override
  390. double get maxExtent => expandedHeight;
  391. @override
  392. double get minExtent => minHeight;
  393. @override
  394. bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
  395. true;
  396. }
  397. class TabBarDelegate extends SliverPersistentHeaderDelegate {
  398. final Widget child;
  399. final double height;
  400. TabBarDelegate({required this.child, required this.height});
  401. @override
  402. Widget build(
  403. BuildContext context,
  404. double shrinkOffset,
  405. bool overlapsContent,
  406. ) {
  407. return SizedBox(height: height, child: child);
  408. }
  409. @override
  410. double get maxExtent => height; // 固定最大高度
  411. @override
  412. double get minExtent => height; // 固定最小高度
  413. @override
  414. bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
  415. true;
  416. }