|
|
@@ -1,16 +1,443 @@
|
|
|
-import 'package:flutter/cupertino.dart';
|
|
|
import 'package:flutter/material.dart';
|
|
|
+import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
+import 'package:get/get.dart';
|
|
|
import 'package:keyboard/base/base_view.dart';
|
|
|
+import 'package:keyboard/resource/string.gen.dart';
|
|
|
|
|
|
+import '../../resource/assets.gen.dart';
|
|
|
import 'character_controller.dart';
|
|
|
|
|
|
class CharacterView extends BaseView<CharacterController> {
|
|
|
const CharacterView({super.key});
|
|
|
|
|
|
-
|
|
|
+ @override
|
|
|
+ backgroundColor() {
|
|
|
+ return Colors.transparent;
|
|
|
+ }
|
|
|
|
|
|
@override
|
|
|
Widget buildBody(BuildContext context) {
|
|
|
- return Center(child: Text("Character", style: TextStyle(color: Colors.black)));
|
|
|
+ return Scaffold(
|
|
|
+ backgroundColor: Color(0xFFF6F5FA),
|
|
|
+ body: Builder(
|
|
|
+ builder: (context) {
|
|
|
+ return NestedScrollView(
|
|
|
+ headerSliverBuilder: (context, innerBoxIsScrolled) {
|
|
|
+ return [
|
|
|
+ /// **🔹 让背景图滑动时裁剪掉上方部分**
|
|
|
+ SliverPersistentHeader(
|
|
|
+ pinned: true,
|
|
|
+ delegate: CharacterHeaderDelegate(
|
|
|
+ expandedHeight: 240.h,
|
|
|
+ minHeight: 100.h,
|
|
|
+ // bottomWidget: SizedBox(),
|
|
|
+ onTap: controller.clickMyKeyboard,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ SliverPersistentHeader(
|
|
|
+ pinned: true,
|
|
|
+
|
|
|
+ // floating: true,
|
|
|
+ delegate: TabBarDelegate(
|
|
|
+ height: 180.h,
|
|
|
+ child: _bottomAppBar(),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ];
|
|
|
+ },
|
|
|
+
|
|
|
+ body: _pages(),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /// **自定义 bottomAppBar**
|
|
|
+ Widget _bottomAppBar() {
|
|
|
+ return Container(
|
|
|
+ decoration: ShapeDecoration(
|
|
|
+ gradient: LinearGradient(
|
|
|
+ begin: Alignment(0.50, -0.00),
|
|
|
+ end: Alignment(0.50, 1.00),
|
|
|
+ colors: [Color(0xFFEAE5FF), Color(0xFFF5F4F9)],
|
|
|
+ ),
|
|
|
+ shape: RoundedRectangleBorder(
|
|
|
+ borderRadius: BorderRadius.only(
|
|
|
+ topLeft: Radius.circular(20.r),
|
|
|
+ topRight: Radius.circular(20.r),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ child: Column(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ _customizeButton(),
|
|
|
+ SizedBox(height: 14.h),
|
|
|
+ Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
+ children: [
|
|
|
+ Assets.images.iconCharacterMarket.image(
|
|
|
+ width: 73.w,
|
|
|
+ height: 25.h,
|
|
|
+ ),
|
|
|
+ Obx(() {
|
|
|
+ return DropdownButton<String>(
|
|
|
+ // hint: Text(''),
|
|
|
+ underline: Container(height: 0),
|
|
|
+ style: TextStyle(
|
|
|
+ color: Colors.black.withAlpha(102),
|
|
|
+ fontSize: 14.sp,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ ),
|
|
|
+
|
|
|
+ icon: Assets.images.iconCharacterArrowDown.image(
|
|
|
+ width: 20.r,
|
|
|
+ height: 20.r,
|
|
|
+ ),
|
|
|
+ value: controller.selectedValue.value,
|
|
|
+ // 绑定 selectedValue
|
|
|
+ onChanged: (String? newValue) {
|
|
|
+ controller.updateSelectedValue(newValue); // 更新选中的值
|
|
|
+ },
|
|
|
+
|
|
|
+ items: List.generate(controller.keyboardOptions.length, (
|
|
|
+ index,
|
|
|
+ ) {
|
|
|
+ String value = controller.keyboardOptions[index];
|
|
|
+ return DropdownMenuItem<String>(
|
|
|
+ value: value,
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ // 让选项文本左对齐
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ // 让 Column 适配内容
|
|
|
+ children: [
|
|
|
+ Padding(
|
|
|
+ padding: EdgeInsets.symmetric(vertical: 8),
|
|
|
+ child: Text(
|
|
|
+ value,
|
|
|
+ style: TextStyle(
|
|
|
+ color: Colors.black.withAlpha(204),
|
|
|
+ fontSize: 14.sp,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (index != controller.keyboardOptions.length - 1)
|
|
|
+ Divider(
|
|
|
+ color: Color(0xFFF6F6F6),
|
|
|
+ thickness: 1,
|
|
|
+ height: 1,
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ }),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ SizedBox(height: 15.h),
|
|
|
+ tabBar(),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 定制按钮
|
|
|
+ Widget _customizeButton() {
|
|
|
+ return Container(
|
|
|
+ margin: EdgeInsets.only(left: 16.w),
|
|
|
+ width: 220.w,
|
|
|
+ height: 56.h,
|
|
|
+ padding: EdgeInsets.symmetric(horizontal: 10.w),
|
|
|
+ decoration: ShapeDecoration(
|
|
|
+ color: const Color(0xFF121212),
|
|
|
+ shape: RoundedRectangleBorder(
|
|
|
+ borderRadius: BorderRadius.circular(40.r),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ child: Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ Assets.images.iconCharacterCustomized.image(
|
|
|
+ width: 36.r,
|
|
|
+ height: 36.r,
|
|
|
+ ),
|
|
|
+ SizedBox(width: 8.w),
|
|
|
+ Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ StringName.goCustomizeCharacter,
|
|
|
+ textAlign: TextAlign.center,
|
|
|
+ style: TextStyle(
|
|
|
+ color: Colors.white,
|
|
|
+ fontSize: 16.sp,
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ Text(
|
|
|
+ StringName.goCustomizeCharacterDesc,
|
|
|
+ style: TextStyle(
|
|
|
+ color: Color(0xFFF5F4F9),
|
|
|
+ fontSize: 11.sp,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ Container(
|
|
|
+ margin: EdgeInsets.only(left: 16.w),
|
|
|
+ width: 24.r,
|
|
|
+ height: 24.r,
|
|
|
+ decoration: ShapeDecoration(
|
|
|
+ color: Colors.white,
|
|
|
+ shape: OvalBorder(),
|
|
|
+ ),
|
|
|
+ child: Assets.images.iconCharacterArrowRight.image(
|
|
|
+ width: 16.r,
|
|
|
+ height: 16.r,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /// **TabBar**
|
|
|
+ Widget tabBar() {
|
|
|
+ return Obx(() {
|
|
|
+ if (controller.characterGroupList.isEmpty) {
|
|
|
+ return const SizedBox.shrink();
|
|
|
+ }
|
|
|
+ return TabBar(
|
|
|
+ controller: controller.tabController.value,
|
|
|
+ dividerHeight: 0,
|
|
|
+ tabAlignment: TabAlignment.start,
|
|
|
+ isScrollable: true,
|
|
|
+ padding: EdgeInsets.symmetric(horizontal: 12.w),
|
|
|
+ labelPadding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
|
+ indicator: const BoxDecoration(),
|
|
|
+ onTap: (index) => controller.onTabChanged(index),
|
|
|
+ tabs: List.generate(controller.characterGroupList.length, (index) {
|
|
|
+ var e = controller.characterGroupList[index];
|
|
|
+ bool isSelected = index == controller.currentTabBarIndex.value;
|
|
|
+ return Container(
|
|
|
+ width: 80.w,
|
|
|
+ height: isSelected ? 38.h : 32.h,
|
|
|
+ decoration:
|
|
|
+ isSelected
|
|
|
+ ? BoxDecoration(
|
|
|
+ borderRadius: BorderRadius.circular(36.r),
|
|
|
+ image: DecorationImage(
|
|
|
+ image:
|
|
|
+ Assets.images.iconCharacterGroupSelected.provider(),
|
|
|
+ fit: BoxFit.cover,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ : BoxDecoration(
|
|
|
+ color: Colors.white.withAlpha(204),
|
|
|
+ borderRadius: BorderRadius.circular(36.r),
|
|
|
+ ),
|
|
|
+ child: Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ children: [
|
|
|
+ if (e.iconUrl != null)
|
|
|
+ Image.network(e.iconUrl!, width: 20.r, height: 20.r),
|
|
|
+ Text(
|
|
|
+ e.name,
|
|
|
+ style: TextStyle(
|
|
|
+ color:
|
|
|
+ isSelected ? Colors.black : Colors.black.withAlpha(104),
|
|
|
+ fontSize: 14.sp,
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _pages() {
|
|
|
+ return Obx(() {
|
|
|
+ if (controller.characterGroupList.isEmpty) {
|
|
|
+ return const Center(child: CircularProgressIndicator());
|
|
|
+ }
|
|
|
+ return PageView(
|
|
|
+ controller: controller.pageController,
|
|
|
+ onPageChanged: (index) {
|
|
|
+ controller.onPageChanged(index);
|
|
|
+ },
|
|
|
+ children:
|
|
|
+ controller.characterGroupList.map((group) {
|
|
|
+ return ListView.builder(
|
|
|
+ itemCount: controller.characterGroupList.length,
|
|
|
+ itemBuilder: (context, index) {
|
|
|
+ return ListTile(title: Text(group.name));
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }).toList(),
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// **🔹 让背景图滑动时裁剪掉上方部分**
|
|
|
+class CharacterHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|
|
+ final double expandedHeight;
|
|
|
+ final double minHeight;
|
|
|
+
|
|
|
+ // final Widget bottomWidget;
|
|
|
+ final VoidCallback onTap;
|
|
|
+
|
|
|
+ CharacterHeaderDelegate({
|
|
|
+ required this.expandedHeight,
|
|
|
+ required this.minHeight,
|
|
|
+ // required this.bottomWidget,
|
|
|
+ required this.onTap,
|
|
|
+ });
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(
|
|
|
+ BuildContext context,
|
|
|
+ double shrinkOffset,
|
|
|
+ bool overlapsContent,
|
|
|
+ ) {
|
|
|
+ final currentVisibleHeight = (expandedHeight - shrinkOffset).clamp(
|
|
|
+ minHeight,
|
|
|
+ expandedHeight,
|
|
|
+ );
|
|
|
+ final tabBarOffset = expandedHeight - currentVisibleHeight; // 计算 TabBar 位移
|
|
|
+
|
|
|
+ final opacity = 1 - currentVisibleHeight / expandedHeight;
|
|
|
+ return Stack(
|
|
|
+ clipBehavior: Clip.none,
|
|
|
+ children: [
|
|
|
+ // 背景图片,动态裁剪
|
|
|
+ Positioned(
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ height: currentVisibleHeight,
|
|
|
+ child: ClipRect(
|
|
|
+ child: Image.asset(
|
|
|
+ Assets.images.bgCharacterBoyBanner.path,
|
|
|
+ fit: BoxFit.cover,
|
|
|
+ height: expandedHeight,
|
|
|
+ alignment: Alignment.topCenter,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+
|
|
|
+ // 遮罩层 Positioned(用于控制背景的可见性)
|
|
|
+ Positioned(
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ height: currentVisibleHeight,
|
|
|
+ child: Container(color: Colors.black.withValues(alpha: opacity)),
|
|
|
+ ),
|
|
|
+ Positioned(
|
|
|
+ top: 0,
|
|
|
+ child: SafeArea(
|
|
|
+ child: GestureDetector(
|
|
|
+ onTap: onTap,
|
|
|
+ child: Container(
|
|
|
+ margin: EdgeInsets.symmetric(horizontal: 16.w),
|
|
|
+ width: 96.w,
|
|
|
+ height: 32.h,
|
|
|
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
|
|
+ decoration: ShapeDecoration(
|
|
|
+ color: Colors.white.withValues(alpha: 153),
|
|
|
+ shape: RoundedRectangleBorder(
|
|
|
+ borderRadius: BorderRadius.circular(10),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ child: Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.start,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
+ spacing: 4.r,
|
|
|
+ children: [
|
|
|
+ Container(
|
|
|
+ width: 24.r,
|
|
|
+ height: 24.r,
|
|
|
+ clipBehavior: Clip.antiAlias,
|
|
|
+ decoration: BoxDecoration(),
|
|
|
+ child: Assets.images.iconCharacterKeyboard.image(
|
|
|
+ width: 24.r,
|
|
|
+ height: 24.r,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ Text(
|
|
|
+ StringName.myKeyboard,
|
|
|
+ textAlign: TextAlign.center,
|
|
|
+ style: TextStyle(
|
|
|
+ color: Colors.black.withAlpha(204),
|
|
|
+ fontSize: 14.sp,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ // TabBar 定位
|
|
|
+ // Positioned(
|
|
|
+ // bottom: tabBarOffset,
|
|
|
+ // left: 0,
|
|
|
+ // right: 0,
|
|
|
+ // child: Transform.translate(
|
|
|
+ // offset: Offset(0, tabBarOffset),
|
|
|
+ // child: bottomWidget,
|
|
|
+ // ),
|
|
|
+ // ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ double get maxExtent => expandedHeight;
|
|
|
+
|
|
|
+ @override
|
|
|
+ double get minExtent => minHeight;
|
|
|
+
|
|
|
+ @override
|
|
|
+ bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
|
|
+ true;
|
|
|
+}
|
|
|
+
|
|
|
+class TabBarDelegate extends SliverPersistentHeaderDelegate {
|
|
|
+ final Widget child;
|
|
|
+ final double height;
|
|
|
+
|
|
|
+ TabBarDelegate({required this.child, required this.height});
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(
|
|
|
+ BuildContext context,
|
|
|
+ double shrinkOffset,
|
|
|
+ bool overlapsContent,
|
|
|
+ ) {
|
|
|
+ return SizedBox(height: height, child: child);
|
|
|
}
|
|
|
-}
|
|
|
+
|
|
|
+ @override
|
|
|
+ double get maxExtent => height; // 固定最大高度
|
|
|
+
|
|
|
+ @override
|
|
|
+ double get minExtent => height; // 固定最小高度
|
|
|
+
|
|
|
+ @override
|
|
|
+ bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
|
|
+ true;
|
|
|
+}
|