character_view.dart 13 KB

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