Browse Source

[feat]新增键盘管理页,调整人设页面布局

云天逵 1 year ago
parent
commit
b899ea4355
82 changed files with 9918 additions and 378 deletions
  1. BIN
      assets/images/bg_character_custom_human.webp
  2. BIN
      assets/images/bg_character_custom_steps.webp
  3. BIN
      assets/images/bg_character_custom_steps_desc.webp
  4. BIN
      assets/images/bg_keyboard_manage.webp
  5. BIN
      assets/images/bg_keyboard_manage_intimacy.webp
  6. BIN
      assets/images/icon_character_custom_button.webp
  7. BIN
      assets/images/icon_character_custom_close.webp
  8. BIN
      assets/images/icon_character_custom_step_one_title.webp
  9. BIN
      assets/images/icon_character_custom_step_three_title.webp
  10. BIN
      assets/images/icon_character_custom_step_two_title.webp
  11. BIN
      assets/images/icon_dialog_close_black.webp
  12. BIN
      assets/images/icon_keyboard_manage_custom.webp
  13. BIN
      assets/images/icon_keyboard_manage_favorite.webp
  14. BIN
      assets/images/icon_keyboard_manage_intimacy_text.webp
  15. BIN
      assets/images/icon_keyboard_manage_plus.webp
  16. BIN
      assets/images/icon_keyboard_manage_x.webp
  17. 11 3
      assets/string/base/string.xml
  18. 23 1
      lib/data/api/atmob_api.dart
  19. 104 0
      lib/data/api/atmob_api.g.dart
  20. 16 0
      lib/data/api/request/keyboard_character_list_request.dart
  21. 70 0
      lib/data/api/request/keyboard_character_list_request.g.dart
  22. 21 0
      lib/data/api/request/keyboard_character_update_request.dart
  23. 77 0
      lib/data/api/request/keyboard_character_update_request.g.dart
  24. 31 0
      lib/data/api/request/keyboard_update_request.dart
  25. 82 0
      lib/data/api/request/keyboard_update_request.g.dart
  26. 2 2
      lib/data/api/response/character_add_response.dart
  27. 4 5
      lib/data/api/response/character_add_response.g.dart
  28. 3 3
      lib/data/api/response/character_page_response.dart
  29. 2 2
      lib/data/api/response/character_unlock_response.dart
  30. 4 5
      lib/data/api/response/character_unlock_response.g.dart
  31. 16 0
      lib/data/api/response/keyboard_character_list_response.dart
  32. 20 0
      lib/data/api/response/keyboard_character_list_response.g.dart
  33. 21 0
      lib/data/bean/character_info.dart
  34. 12 0
      lib/data/bean/character_info.g.dart
  35. 4 0
      lib/data/bean/keyboard_info.dart
  36. 2 0
      lib/data/bean/keyboard_info.g.dart
  37. 0 2
      lib/data/repository/account_repository.dart
  38. 4 4
      lib/data/repository/characters_repository.dart
  39. 55 0
      lib/data/repository/keyboard_repository.dart
  40. 8 0
      lib/di/get_it.config.dart
  41. 32 26
      lib/dialog/character_details_dialog.dart
  42. 12 2
      lib/module/character/character_controller.dart
  43. 299 289
      lib/module/character/character_view.dart
  44. 48 15
      lib/module/character/content/character_group_content_controller.dart
  45. 21 16
      lib/module/character/content/character_group_content_view.dart
  46. 20 0
      lib/module/character_custom/character_custom_controller.dart
  47. 306 0
      lib/module/character_custom/character_custom_page.dart
  48. 503 0
      lib/module/keyboard_manage/keyboard_manage_controller.dart
  49. 545 0
      lib/module/keyboard_manage/keyboard_manage_page.dart
  50. 2 2
      lib/plugins/keyboard_android_platform.dart
  51. 84 0
      lib/resource/assets.gen.dart
  52. 20 0
      lib/resource/string.gen.dart
  53. 10 0
      lib/router/app_pages.dart
  54. 90 0
      lib/widget/gradient_rect_slider_track_shape.dart
  55. 33 0
      lib/widget/tab_custom_gradient_indicator.dart
  56. 73 0
      plugins/reorderables/.gitignore
  57. 10 0
      plugins/reorderables/.metadata
  58. 113 0
      plugins/reorderables/CHANGELOG.md
  59. 21 0
      plugins/reorderables/LICENSE
  60. 503 0
      plugins/reorderables/README.md
  61. 7 0
      plugins/reorderables/lib/reorderables.dart
  62. 902 0
      plugins/reorderables/lib/src/rendering/tabluar_flex.dart
  63. 73 0
      plugins/reorderables/lib/src/rendering/transitions.dart
  64. 505 0
      plugins/reorderables/lib/src/rendering/wrap.dart
  65. 36 0
      plugins/reorderables/lib/src/widgets/basic.dart
  66. 601 0
      plugins/reorderables/lib/src/widgets/passthrough_overlay.dart
  67. 1107 0
      plugins/reorderables/lib/src/widgets/reorderable_flex.dart
  68. 63 0
      plugins/reorderables/lib/src/widgets/reorderable_mixin.dart
  69. 1015 0
      plugins/reorderables/lib/src/widgets/reorderable_sliver.dart
  70. 287 0
      plugins/reorderables/lib/src/widgets/reorderable_table.dart
  71. 23 0
      plugins/reorderables/lib/src/widgets/reorderable_widget.dart
  72. 1287 0
      plugins/reorderables/lib/src/widgets/reorderable_wrap.dart
  73. 11 0
      plugins/reorderables/lib/src/widgets/safe_state.dart
  74. 452 0
      plugins/reorderables/lib/src/widgets/tabluar_flex.dart
  75. 55 0
      plugins/reorderables/lib/src/widgets/transitions.dart
  76. 9 0
      plugins/reorderables/lib/src/widgets/typedefs.dart
  77. 57 0
      plugins/reorderables/lib/src/widgets/wrap.dart
  78. 53 0
      plugins/reorderables/pubspec.yaml
  79. 0 0
      plugins/reorderables/res/values/strings_en.arb
  80. 13 0
      plugins/reorderables/test/reorderables_test.dart
  81. 16 1
      pubspec.lock
  82. 9 0
      pubspec.yaml

BIN
assets/images/bg_character_custom_human.webp


BIN
assets/images/bg_character_custom_steps.webp


BIN
assets/images/bg_character_custom_steps_desc.webp


BIN
assets/images/bg_keyboard_manage.webp


BIN
assets/images/bg_keyboard_manage_intimacy.webp


BIN
assets/images/icon_character_custom_button.webp


BIN
assets/images/icon_character_custom_close.webp


BIN
assets/images/icon_character_custom_step_one_title.webp


BIN
assets/images/icon_character_custom_step_three_title.webp


BIN
assets/images/icon_character_custom_step_two_title.webp


BIN
assets/images/icon_dialog_close_black.webp


BIN
assets/images/icon_keyboard_manage_custom.webp


BIN
assets/images/icon_keyboard_manage_favorite.webp


BIN
assets/images/icon_keyboard_manage_intimacy_text.webp


BIN
assets/images/icon_keyboard_manage_plus.webp


BIN
assets/images/icon_keyboard_manage_x.webp


+ 11 - 3
assets/string/base/string.xml

@@ -84,8 +84,16 @@
     <string name="add_to_keyboard">添加到键盘</string>
     <string name="added_to_keyboard">已添加到键盘</string>
 
-
-
-
+    <!--    键盘管理页面-->
+    <string name="keyboard_custom">定制键盘</string>
+    <string name="general_keyboard">通用键盘</string>
+    <string name="intimacy">亲密度</string>
+    <string name="keyboard_character">键盘人设</string>
+    <string name="keyboard_save">保存</string>
+    <string name="keyboard_save_success">保存成功</string>
+    <string name="keyboard_save_failed">保存失败</string>
+    <string name="add_character">添加人设</string>
+    <string name="custom_character">定制人设</string>
+    <string name="character_custom_steps_desc">专属于你独一无二的人设</string>
 
 </resources>

+ 23 - 1
lib/data/api/atmob_api.dart

@@ -5,7 +5,10 @@ import 'package:keyboard/data/api/request/character_page_request.dart';
 import 'package:keyboard/data/api/request/character_unlock_request.dart';
 import 'package:keyboard/data/api/request/complaint_submit_request.dart';
 import 'package:keyboard/data/api/request/config_request.dart';
+import 'package:keyboard/data/api/request/keyboard_character_list_request.dart';
+import 'package:keyboard/data/api/request/keyboard_character_update_request.dart';
 import 'package:keyboard/data/api/request/keyboard_list_request.dart';
+import 'package:keyboard/data/api/request/keyboard_update_request.dart';
 import 'package:keyboard/data/api/request/login_request.dart';
 import 'package:keyboard/data/api/request/send_code_request.dart';
 import 'package:keyboard/data/api/request/user_info_setting_request.dart';
@@ -14,6 +17,7 @@ import 'package:keyboard/data/api/response/character_group_response.dart';
 import 'package:keyboard/data/api/response/character_page_response.dart';
 import 'package:keyboard/data/api/response/character_unlock_response.dart';
 import 'package:keyboard/data/api/response/config_response.dart';
+import 'package:keyboard/data/api/response/keyboard_character_list_response.dart';
 import 'package:keyboard/data/api/response/keyboard_list_response.dart';
 import 'package:keyboard/data/api/response/login_response.dart';
 import 'package:keyboard/data/api/response/new_user_get_character_response.dart';
@@ -88,9 +92,27 @@ abstract class AtmobApi {
     @Body() CharacterAddRequest request,
   );
 
+  // 获取键盘人设列表
+  @POST("/project/keyboard/v1/character/list")
+  Future<BaseResponse<KeyboardCharacterListResponse>> getKeyboardCharacterList(
+    @Body() KeyboardCharacterListRequest request,
+  );
+
+  //更新键盘人设
+  @POST("/project/keyboard/v1/character/keyboard/update")
+  Future<BaseResponse> keyboardCharacterUpdate(
+    @Body() KeyboardCharacterUpdateRequest request,
+  );
+
   // 获取键盘列表
   @POST("/project/keyboard/v1/keyboard/list")
-  Future<BaseResponse<KeyboardListResponse>> getKeyboardList(@Body() KeyboardListRequest request);
+  Future<BaseResponse<KeyboardListResponse>> getKeyboardList(
+    @Body() KeyboardListRequest request,
+  );
+
+  //  更新键盘信息
+  @POST("/project/keyboard/v1/keyboard/update")
+  Future<BaseResponse> keyboardUpdate(@Body() KeyboardUpdateRequest request);
 
   //获取配置信息
   @POST("/project/keyboard/v1/confs")

+ 104 - 0
lib/data/api/atmob_api.g.dart

@@ -379,6 +379,77 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
+  Future<BaseResponse<KeyboardCharacterListResponse>> getKeyboardCharacterList(
+    KeyboardCharacterListRequest request,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options =
+        _setStreamType<BaseResponse<KeyboardCharacterListResponse>>(
+          Options(method: 'POST', headers: _headers, extra: _extra)
+              .compose(
+                _dio.options,
+                '/project/keyboard/v1/character/list',
+                queryParameters: queryParameters,
+                data: _data,
+              )
+              .copyWith(
+                baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
+              ),
+        );
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<KeyboardCharacterListResponse> _value;
+    try {
+      _value = BaseResponse<KeyboardCharacterListResponse>.fromJson(
+        _result.data!,
+        (json) => KeyboardCharacterListResponse.fromJson(
+          json as Map<String, dynamic>,
+        ),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<dynamic>> keyboardCharacterUpdate(
+    KeyboardCharacterUpdateRequest request,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<dynamic>>(
+      Options(method: 'POST', headers: _headers, extra: _extra)
+          .compose(
+            _dio.options,
+            '/project/keyboard/v1/character/keyboard/update',
+            queryParameters: queryParameters,
+            data: _data,
+          )
+          .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
+    );
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<dynamic> _value;
+    try {
+      _value = BaseResponse<dynamic>.fromJson(
+        _result.data!,
+        (json) => json as dynamic,
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
   Future<BaseResponse<KeyboardListResponse>> getKeyboardList(
     KeyboardListRequest request,
   ) async {
@@ -412,6 +483,39 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
+  Future<BaseResponse<dynamic>> keyboardUpdate(
+    KeyboardUpdateRequest request,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<dynamic>>(
+      Options(method: 'POST', headers: _headers, extra: _extra)
+          .compose(
+            _dio.options,
+            '/project/keyboard/v1/keyboard/update',
+            queryParameters: queryParameters,
+            data: _data,
+          )
+          .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
+    );
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<dynamic> _value;
+    try {
+      _value = BaseResponse<dynamic>.fromJson(
+        _result.data!,
+        (json) => json as dynamic,
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
   Future<BaseResponse<ConfigResponse>> confs(ConfigRequest request) async {
     final _extra = <String, dynamic>{};
     final queryParameters = <String, dynamic>{};

+ 16 - 0
lib/data/api/request/keyboard_character_list_request.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/app_base_request.dart';
+
+part 'keyboard_character_list_request.g.dart';
+
+@JsonSerializable()
+class KeyboardCharacterListRequest extends AppBaseRequest {
+  @JsonKey(name: "keyboardId")
+  String keyboardId;
+
+  KeyboardCharacterListRequest({required this.keyboardId});
+
+  @override
+  Map<String, dynamic> toJson() => _$KeyboardCharacterListRequestToJson(this);
+}

+ 70 - 0
lib/data/api/request/keyboard_character_list_request.g.dart

@@ -0,0 +1,70 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'keyboard_character_list_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+KeyboardCharacterListRequest _$KeyboardCharacterListRequestFromJson(
+  Map<String, dynamic> json,
+) =>
+    KeyboardCharacterListRequest(keyboardId: json['keyboardId'] as String)
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$KeyboardCharacterListRequestToJson(
+  KeyboardCharacterListRequest instance,
+) => <String, dynamic>{
+  'appPlatform': instance.appPlatform,
+  'os': instance.os,
+  'osVersion': instance.osVersion,
+  'packageName': instance.packageName,
+  'appVersionName': instance.appVersionName,
+  'appVersionCode': instance.appVersionCode,
+  'channelName': instance.channelName,
+  'appId': instance.appId,
+  'tgPlatform': instance.tgPlatform,
+  'oaid': instance.oaid,
+  'aaid': instance.aaid,
+  'androidId': instance.androidId,
+  'imei': instance.imei,
+  'simImei0': instance.simImei0,
+  'simImei1': instance.simImei1,
+  'mac': instance.mac,
+  'idfa': instance.idfa,
+  'idfv': instance.idfv,
+  'machineId': instance.machineId,
+  'brand': instance.brand,
+  'model': instance.model,
+  'wifiName': instance.wifiName,
+  'region': instance.region,
+  'locLng': instance.locLng,
+  'locLat': instance.locLat,
+  'authToken': instance.authToken,
+  'keyboardId': instance.keyboardId,
+};

+ 21 - 0
lib/data/api/request/keyboard_character_update_request.dart

@@ -0,0 +1,21 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/app_base_request.dart';
+
+part 'keyboard_character_update_request.g.dart';
+
+@JsonSerializable()
+class KeyboardCharacterUpdateRequest extends AppBaseRequest {
+  @JsonKey(name: "keyboardId")
+  String keyboardId;
+  @JsonKey(name: "characterIds")
+  List<String> characterIds;
+
+  KeyboardCharacterUpdateRequest({
+    required this.keyboardId,
+    required this.characterIds,
+  });
+
+  @override
+  Map<String, dynamic> toJson() => _$KeyboardCharacterUpdateRequestToJson(this);
+}

+ 77 - 0
lib/data/api/request/keyboard_character_update_request.g.dart

@@ -0,0 +1,77 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'keyboard_character_update_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+KeyboardCharacterUpdateRequest _$KeyboardCharacterUpdateRequestFromJson(
+  Map<String, dynamic> json,
+) =>
+    KeyboardCharacterUpdateRequest(
+        keyboardId: json['keyboardId'] as String,
+        characterIds:
+            (json['characterIds'] as List<dynamic>)
+                .map((e) => e as String)
+                .toList(),
+      )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$KeyboardCharacterUpdateRequestToJson(
+  KeyboardCharacterUpdateRequest instance,
+) => <String, dynamic>{
+  'appPlatform': instance.appPlatform,
+  'os': instance.os,
+  'osVersion': instance.osVersion,
+  'packageName': instance.packageName,
+  'appVersionName': instance.appVersionName,
+  'appVersionCode': instance.appVersionCode,
+  'channelName': instance.channelName,
+  'appId': instance.appId,
+  'tgPlatform': instance.tgPlatform,
+  'oaid': instance.oaid,
+  'aaid': instance.aaid,
+  'androidId': instance.androidId,
+  'imei': instance.imei,
+  'simImei0': instance.simImei0,
+  'simImei1': instance.simImei1,
+  'mac': instance.mac,
+  'idfa': instance.idfa,
+  'idfv': instance.idfv,
+  'machineId': instance.machineId,
+  'brand': instance.brand,
+  'model': instance.model,
+  'wifiName': instance.wifiName,
+  'region': instance.region,
+  'locLng': instance.locLng,
+  'locLat': instance.locLat,
+  'authToken': instance.authToken,
+  'keyboardId': instance.keyboardId,
+  'characterIds': instance.characterIds,
+};

+ 31 - 0
lib/data/api/request/keyboard_update_request.dart

@@ -0,0 +1,31 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/app_base_request.dart';
+
+part 'keyboard_update_request.g.dart';
+
+@JsonSerializable()
+class KeyboardUpdateRequest extends AppBaseRequest {
+  @JsonKey(name: "keyboardId")
+  String keyboardId;
+
+  @JsonKey(name: "intimacy")
+  int? intimacy;
+
+  @JsonKey(name: "name")
+  String? name;
+
+  @JsonKey(name: "gender")
+  int? gender;
+
+  @JsonKey(name: "birthday")
+  String? birthday;
+
+  @JsonKey(name: "imageUrl")
+  String? imageUrl;
+
+  KeyboardUpdateRequest({required this.keyboardId,this.intimacy,this.name,this.gender,this.birthday,this.imageUrl,});
+
+  @override
+  Map<String, dynamic> toJson() => _$KeyboardUpdateRequestToJson(this);
+}

+ 82 - 0
lib/data/api/request/keyboard_update_request.g.dart

@@ -0,0 +1,82 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'keyboard_update_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+KeyboardUpdateRequest _$KeyboardUpdateRequestFromJson(
+  Map<String, dynamic> json,
+) =>
+    KeyboardUpdateRequest(
+        keyboardId: json['keyboardId'] as String,
+        intimacy: (json['intimacy'] as num?)?.toInt(),
+        name: json['name'] as String?,
+        gender: (json['gender'] as num?)?.toInt(),
+        birthday: json['birthday'] as String?,
+        imageUrl: json['imageUrl'] as String?,
+      )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$KeyboardUpdateRequestToJson(
+  KeyboardUpdateRequest instance,
+) => <String, dynamic>{
+  'appPlatform': instance.appPlatform,
+  'os': instance.os,
+  'osVersion': instance.osVersion,
+  'packageName': instance.packageName,
+  'appVersionName': instance.appVersionName,
+  'appVersionCode': instance.appVersionCode,
+  'channelName': instance.channelName,
+  'appId': instance.appId,
+  'tgPlatform': instance.tgPlatform,
+  'oaid': instance.oaid,
+  'aaid': instance.aaid,
+  'androidId': instance.androidId,
+  'imei': instance.imei,
+  'simImei0': instance.simImei0,
+  'simImei1': instance.simImei1,
+  'mac': instance.mac,
+  'idfa': instance.idfa,
+  'idfv': instance.idfv,
+  'machineId': instance.machineId,
+  'brand': instance.brand,
+  'model': instance.model,
+  'wifiName': instance.wifiName,
+  'region': instance.region,
+  'locLng': instance.locLng,
+  'locLat': instance.locLat,
+  'authToken': instance.authToken,
+  'keyboardId': instance.keyboardId,
+  'intimacy': instance.intimacy,
+  'name': instance.name,
+  'gender': instance.gender,
+  'birthday': instance.birthday,
+  'imageUrl': instance.imageUrl,
+};

+ 2 - 2
lib/data/api/response/character_add_response.dart

@@ -8,8 +8,8 @@ part 'character_add_response.g.dart';
 @JsonSerializable()
 class CharacterAddResponse {
 
-  @JsonKey(name: "characterInfos")
-  late final List<CharacterInfo> characterInfo;
+  @JsonKey(name: "characterInfo")
+  late final CharacterInfo characterInfo;
 
   CharacterAddResponse({ required this.characterInfo});
 

+ 4 - 5
lib/data/api/response/character_add_response.g.dart

@@ -9,12 +9,11 @@ part of 'character_add_response.dart';
 CharacterAddResponse _$CharacterAddResponseFromJson(
   Map<String, dynamic> json,
 ) => CharacterAddResponse(
-  characterInfo:
-      (json['characterInfos'] as List<dynamic>)
-          .map((e) => CharacterInfo.fromJson(e as Map<String, dynamic>))
-          .toList(),
+  characterInfo: CharacterInfo.fromJson(
+    json['characterInfo'] as Map<String, dynamic>,
+  ),
 );
 
 Map<String, dynamic> _$CharacterAddResponseToJson(
   CharacterAddResponse instance,
-) => <String, dynamic>{'characterInfos': instance.characterInfo};
+) => <String, dynamic>{'characterInfo': instance.characterInfo};

+ 3 - 3
lib/data/api/response/character_page_response.dart

@@ -1,16 +1,16 @@
+import 'package:get/get_connect/http/src/request/request.dart';
 import 'package:json_annotation/json_annotation.dart';
 
 import '../../bean/character_info.dart';
 
-
 part 'character_page_response.g.dart';
 
 @JsonSerializable()
 class CharacterPageResponse {
   @JsonKey(name: "count")
-  late final int count;
+  int count;
   @JsonKey(name: "list")
-  late final List<CharacterInfo> characterInfos;
+  List<CharacterInfo> characterInfos;
 
   CharacterPageResponse({required this.count, required this.characterInfos});
 

+ 2 - 2
lib/data/api/response/character_unlock_response.dart

@@ -8,8 +8,8 @@ part 'character_unlock_response.g.dart';
 @JsonSerializable()
 class CharacterUnlockResponse {
 
-  @JsonKey(name: "characterInfos")
-  late final List<CharacterInfo> characterInfo;
+  @JsonKey(name: "characterInfo")
+  late final CharacterInfo characterInfo;
 
   CharacterUnlockResponse({ required this.characterInfo});
 

+ 4 - 5
lib/data/api/response/character_unlock_response.g.dart

@@ -9,12 +9,11 @@ part of 'character_unlock_response.dart';
 CharacterUnlockResponse _$CharacterUnlockResponseFromJson(
   Map<String, dynamic> json,
 ) => CharacterUnlockResponse(
-  characterInfo:
-      (json['characterInfos'] as List<dynamic>)
-          .map((e) => CharacterInfo.fromJson(e as Map<String, dynamic>))
-          .toList(),
+  characterInfo: CharacterInfo.fromJson(
+    json['characterInfo'] as Map<String, dynamic>,
+  ),
 );
 
 Map<String, dynamic> _$CharacterUnlockResponseToJson(
   CharacterUnlockResponse instance,
-) => <String, dynamic>{'characterInfos': instance.characterInfo};
+) => <String, dynamic>{'characterInfo': instance.characterInfo};

+ 16 - 0
lib/data/api/response/keyboard_character_list_response.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/character_info.dart';
+
+part 'keyboard_character_list_response.g.dart';
+
+@JsonSerializable()
+class KeyboardCharacterListResponse {
+  @JsonKey(name: "characterInfos")
+  List<CharacterInfo> characterInfos;
+
+  KeyboardCharacterListResponse({required this.characterInfos});
+
+  factory KeyboardCharacterListResponse.fromJson(Map<String, dynamic> json) =>
+      _$KeyboardCharacterListResponseFromJson(json);
+}

+ 20 - 0
lib/data/api/response/keyboard_character_list_response.g.dart

@@ -0,0 +1,20 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'keyboard_character_list_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+KeyboardCharacterListResponse _$KeyboardCharacterListResponseFromJson(
+  Map<String, dynamic> json,
+) => KeyboardCharacterListResponse(
+  characterInfos:
+      (json['characterInfos'] as List<dynamic>)
+          .map((e) => CharacterInfo.fromJson(e as Map<String, dynamic>))
+          .toList(),
+);
+
+Map<String, dynamic> _$KeyboardCharacterListResponseToJson(
+  KeyboardCharacterListResponse instance,
+) => <String, dynamic>{'characterInfos': instance.characterInfos};

+ 21 - 0
lib/data/bean/character_info.dart

@@ -24,15 +24,32 @@ class CharacterInfo {
   @JsonKey(name: 'emoji')
   String? emoji;
 
+  // 是否是VIP可用
   @JsonKey(name: 'isVip')
   bool? isVip;
 
+  // 是否锁定
   @JsonKey(name: 'isLock')
   bool? isLock;
 
+  // 是否已添加
   @JsonKey(name: 'isAdd')
   bool? isAdd;
 
+  // 爱好列表(定制人设)
+  @JsonKey(name: "hobbies")
+  List<String>? hobbies;
+
+  // 性格列表(定制人设)
+  @JsonKey(name: "characters")
+  List<String>? characters;
+
+  @JsonKey(name: "birthday")
+  String? birthday;
+
+  @JsonKey(name: "gender")
+  int? gender;
+
   CharacterInfo({
     required this.id,
     this.name,
@@ -42,6 +59,10 @@ class CharacterInfo {
     this.isVip,
     this.isLock,
     this.isAdd,
+    this.hobbies,
+    this.characters,
+    this.birthday,
+    this.gender,
   });
 
   factory CharacterInfo.fromJson(Map<String, dynamic> json) =>

+ 12 - 0
lib/data/bean/character_info.g.dart

@@ -16,6 +16,14 @@ CharacterInfo _$CharacterInfoFromJson(Map<String, dynamic> json) =>
       isVip: json['isVip'] as bool?,
       isLock: json['isLock'] as bool?,
       isAdd: json['isAdd'] as bool?,
+      hobbies:
+          (json['hobbies'] as List<dynamic>?)?.map((e) => e as String).toList(),
+      characters:
+          (json['characters'] as List<dynamic>?)
+              ?.map((e) => e as String)
+              .toList(),
+      birthday: json['birthday'] as String?,
+      gender: (json['gender'] as num?)?.toInt(),
     );
 
 Map<String, dynamic> _$CharacterInfoToJson(CharacterInfo instance) =>
@@ -28,4 +36,8 @@ Map<String, dynamic> _$CharacterInfoToJson(CharacterInfo instance) =>
       'isVip': instance.isVip,
       'isLock': instance.isLock,
       'isAdd': instance.isAdd,
+      'hobbies': instance.hobbies,
+      'characters': instance.characters,
+      'birthday': instance.birthday,
+      'gender': instance.gender,
     };

+ 4 - 0
lib/data/bean/keyboard_info.dart

@@ -32,6 +32,9 @@ class KeyboardInfo {
   @JsonKey(name: 'imageUrl')
   String? imageUrl;
 
+  @JsonKey(name: 'isChoose')
+  bool? isChoose;
+
   KeyboardInfo({
     this.id,
     this.type,
@@ -40,6 +43,7 @@ class KeyboardInfo {
     this.birthday,
     this.intimacy,
     this.imageUrl,
+    this.isChoose,
   });
 
   factory KeyboardInfo.fromJson(Map<String, dynamic> json) =>

+ 2 - 0
lib/data/bean/keyboard_info.g.dart

@@ -14,6 +14,7 @@ KeyboardInfo _$KeyboardInfoFromJson(Map<String, dynamic> json) => KeyboardInfo(
   birthday: json['birthday'] as String?,
   intimacy: (json['intimacy'] as num?)?.toInt(),
   imageUrl: json['imageUrl'] as String?,
+  isChoose: json['isChoose'] as bool?,
 );
 
 Map<String, dynamic> _$KeyboardInfoToJson(KeyboardInfo instance) =>
@@ -25,4 +26,5 @@ Map<String, dynamic> _$KeyboardInfoToJson(KeyboardInfo instance) =>
       'birthday': instance.birthday,
       'intimacy': instance.intimacy,
       'imageUrl': instance.imageUrl,
+      'isChoose': instance.isChoose,
     };

+ 0 - 2
lib/data/repository/account_repository.dart

@@ -140,8 +140,6 @@ class AccountRepository {
   }
 
   void onLoginSuccess(String phoneNum, String authToken) {
-    KVUtil.putString(keyAccountLoginPhoneNum, null);
-    KVUtil.putString(keyAccountLoginToken, null);
 
     AccountRepository.token = authToken;
     loginPhoneNum.value = phoneNum;

+ 4 - 4
lib/data/repository/characters_repository.dart

@@ -1,6 +1,8 @@
 import 'package:injectable/injectable.dart';
 
 import 'package:get/get.dart';
+import 'package:keyboard/data/api/response/character_add_response.dart';
+import 'package:keyboard/data/api/response/character_unlock_response.dart';
 import 'package:keyboard/data/repository/keyboard_repository.dart';
 import '../../base/app_base_request.dart';
 import '../../di/get_it.dart';
@@ -61,7 +63,7 @@ class CharactersRepository {
   }
 
   // 添加人设
-  Future<void> characterAdd({
+  Future<CharacterAddResponse> characterAdd({
     required String characterId,
     required String keyboardId,
   }) {
@@ -73,7 +75,7 @@ class CharactersRepository {
   }
 
   // 解锁人设
-  Future<void> characterUnlock({
+  Future<CharacterUnlockResponse> characterUnlock({
     required String characterId,
     required String keyboardId,
   }) {
@@ -87,8 +89,6 @@ class CharactersRepository {
         .then(HttpHandler.handle(false));
   }
 
-
-
   static CharactersRepository getInstance() =>
       getIt.get<CharactersRepository>();
 }

+ 55 - 0
lib/data/repository/keyboard_repository.dart

@@ -1,10 +1,14 @@
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
+import 'package:keyboard/data/api/request/keyboard_character_update_request.dart';
 import 'package:keyboard/data/api/request/keyboard_list_request.dart';
 
 import '../../di/get_it.dart';
 import '../../utils/http_handler.dart';
 import '../api/atmob_api.dart';
+import '../api/request/keyboard_character_list_request.dart';
+import '../api/request/keyboard_update_request.dart';
+import '../api/response/keyboard_character_list_response.dart';
 import '../api/response/keyboard_list_response.dart';
 import '../bean/keyboard_info.dart';
 
@@ -12,6 +16,7 @@ import '../bean/keyboard_info.dart';
 class KeyboardRepository {
   final tag = "KeyboardRepository";
   final AtmobApi atmobApi;
+
   final RxList<KeyboardInfo> _keyboardInfoList = RxList();
 
   RxList<KeyboardInfo> get keyboardInfoList => _keyboardInfoList;
@@ -27,11 +32,61 @@ class KeyboardRepository {
     });
   }
 
+  // 获取键盘列表
   Future<KeyboardListResponse> getKeyboardList({String? type}) {
     return atmobApi
         .getKeyboardList(KeyboardListRequest(type: type))
         .then(HttpHandler.handle(true));
   }
 
+  // 获取键盘人设列表
+  Future<KeyboardCharacterListResponse> getKeyboardCharacterList({
+    required String keyboardId,
+  }) {
+    return atmobApi
+        .getKeyboardCharacterList(
+          KeyboardCharacterListRequest(keyboardId: keyboardId),
+        )
+        .then(HttpHandler.handle(true));
+  }
+
+  //更新键盘人设
+  Future<void> keyboardCharacterUpdate({
+    required List<String> characterIds,
+    required String keyboardId,
+  }) {
+    return atmobApi
+        .keyboardCharacterUpdate(
+          KeyboardCharacterUpdateRequest(
+            keyboardId: keyboardId,
+            characterIds: characterIds,
+          ),
+        )
+        .then(HttpHandler.handle(true));
+  }
+
+  // 更新键盘信息
+  Future<void> updateKeyboardInfo({
+    required String keyboardId,
+    String? name,
+    String? imageUrl,
+    String? birthday,
+    int? intimacy,
+    int? gender,
+  }) {
+    return atmobApi
+        .keyboardUpdate(
+          KeyboardUpdateRequest(
+            keyboardId: keyboardId,
+            name: name,
+            imageUrl: imageUrl,
+            birthday: birthday,
+            intimacy: intimacy,
+            gender: gender,
+          ),
+        )
+        .then(HttpHandler.handle(true));
+  }
+
   static KeyboardRepository getInstance() => getIt.get<KeyboardRepository>();
 }

+ 8 - 0
lib/di/get_it.config.dart

@@ -25,7 +25,9 @@ import '../module/browser/browser_controller.dart' as _i923;
 import '../module/character/character_controller.dart' as _i888;
 import '../module/character/content/character_group_content_controller.dart'
     as _i970;
+import '../module/character_custom/character_custom_controller.dart' as _i15;
 import '../module/feedback/feedback_controller.dart' as _i876;
+import '../module/keyboard_manage/keyboard_manage_controller.dart' as _i922;
 import '../module/login/login_controller.dart' as _i1008;
 import '../module/main/main_controller.dart' as _i731;
 import '../module/mine/mine_controller.dart' as _i732;
@@ -42,6 +44,9 @@ extension GetItInjectableX on _i174.GetIt {
     final networkModule = _$NetworkModule();
     gh.factory<_i256.AboutController>(() => _i256.AboutController());
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
+    gh.factory<_i15.CharacterCustomController>(
+      () => _i15.CharacterCustomController(),
+    );
     gh.factory<_i1008.LoginController>(() => _i1008.LoginController());
     gh.factory<_i731.MainController>(() => _i731.MainController());
     gh.singleton<_i361.Dio>(
@@ -80,6 +85,9 @@ extension GetItInjectableX on _i174.GetIt {
     gh.lazySingleton<_i274.KeyboardRepository>(
       () => _i274.KeyboardRepository(gh<_i243.AtmobApi>()),
     );
+    gh.factory<_i922.KeyboardManageController>(
+      () => _i922.KeyboardManageController(gh<_i274.KeyboardRepository>()),
+    );
     gh.factory<_i876.FeedbackController>(
       () => _i876.FeedbackController(gh<_i83.AccountRepository>()),
     );

+ 32 - 26
lib/dialog/character_details_dialog.dart

@@ -105,35 +105,41 @@ class CharacterDetailsDialog {
                                 height: 32.r,
                               ),
                               SizedBox(width: 10.w),
-                              Column(
-                                crossAxisAlignment: CrossAxisAlignment.start,
-                                children: [
-                                  Row(
-                                    children: [
-                                      Text(
-                                        characterInfo.name ?? "",
-                                        style: TextStyle(
-                                          color: Colors.black.withAlpha(204),
-                                          fontSize: 14.sp,
-                                          fontWeight: FontWeight.w700,
+                              Expanded(
+                                child: Column(
+                                  crossAxisAlignment: CrossAxisAlignment.start,
+                                  children: [
+                                    Row(
+                                      children: [
+                                        Text(
+                                          characterInfo.name ?? "",
+                                          style: TextStyle(
+                                            color: Colors.black.withAlpha(204),
+                                            fontSize: 14.sp,
+                                            fontWeight: FontWeight.w700,
+                                          ),
                                         ),
+                                        SizedBox(width: 4.w),
+                                        characterInfo.isVip == true
+                                            ? Assets.images.iconCharacterVip
+                                                .image(
+                                                  width: 38.w,
+                                                  height: 16.h,
+                                                )
+                                            : Container(),
+                                      ],
+                                    ),
+                                    Text(
+                                      characterInfo.description ?? "",
+                                      softWrap: true,
+                                      style: TextStyle(
+                                        color: Colors.black.withAlpha(153),
+                                        fontSize: 12.sp,
+                                        fontWeight: FontWeight.w400,
                                       ),
-                                      SizedBox(width: 4.w),
-                                      characterInfo.isVip == true
-                                          ? Assets.images.iconCharacterVip
-                                              .image(width: 38.w, height: 16.h)
-                                          : Container(),
-                                    ],
-                                  ),
-                                  Text(
-                                    characterInfo.description ?? "",
-                                    style: TextStyle(
-                                      color: Colors.black.withAlpha(153),
-                                      fontSize: 12.sp,
-                                      fontWeight: FontWeight.w400,
                                     ),
-                                  ),
-                                ],
+                                  ],
+                                ),
                               ),
                             ],
                           ),

+ 12 - 2
lib/module/character/character_controller.dart

@@ -4,11 +4,13 @@ import 'package:injectable/injectable.dart';
 import 'package:keyboard/base/base_controller.dart';
 import 'package:keyboard/data/bean/keyboard_info.dart';
 import 'package:keyboard/data/repository/config_repository.dart';
+import 'package:keyboard/module/keyboard_manage/keyboard_manage_page.dart';
 import 'package:keyboard/utils/atmob_log.dart';
 
 import '../../data/bean/character_group_info.dart';
 import '../../data/repository/characters_repository.dart';
 import '../../data/repository/keyboard_repository.dart';
+import '../character_custom/character_custom_page.dart';
 
 @injectable
 class CharacterController extends BaseController
@@ -119,13 +121,21 @@ class CharacterController extends BaseController
     super.onClose();
   }
 
-  clickMyKeyboard() {
+ void clickMyKeyboard() {
     AtmobLog.d(tag, "clickMyKeyboard");
+    KeyboardManagePage.start();
   }
 
 
+ void  clickCustomCharacter() {
+    AtmobLog.d(tag, "clickCustomCharacter");
+    CharacterCustomPage.start();
 
-  void updateSelectedValue(String? newValue) {
+  }
+
+
+  // 切换键盘
+  void switchKeyboard(String? newValue) {
     currentKeyboardInfo.value = keyboardInfoList.firstWhere(
       (element) => element.name == newValue,
       orElse: () => keyboardInfoList.first,

+ 299 - 289
lib/module/character/character_view.dart

@@ -19,197 +19,231 @@ class CharacterView extends BaseView<CharacterController> {
 
   @override
   Widget buildBody(BuildContext context) {
+
     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,
+      body: Stack(
+        children: [
+          Builder(
+            builder: (context) {
+              return Column(
+                children: [
+                  Expanded(
+                    child: NestedScrollView(
+                      headerSliverBuilder: (context, innerBoxIsScrolled) {
+                        return [
+                          SliverPersistentHeader(
+                            pinned: true,
+                            delegate: CharacterHeaderDelegate(
+                              expandedHeight: 380.h, //调整照片位置
+                              minHeight: 270.h,
+                              bottomWidget: _bottomAppBar(),
+                              onTap: controller.clickMyKeyboard,
+                            ),
+                          ),
+                        ];
+                      },
 
-                  // floating: true,
-                  delegate: TabBarDelegate(
-                    height: 180.h,
-                    child: _bottomAppBar(),
+                      body: _pages(),
+                    ),
                   ),
-                ),
-              ];
+                ],
+              );
             },
-
-            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),
-          ),
+    return Column(
+      children: [
+        Stack(
+          children: [
+            Column(
+              children: [
+                SizedBox(height: 31.h),
+                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: [
+                      // 定义按钮与人设之间的间距
+                      SizedBox(height: 53.h),
+                    // 人设市场标识和下拉框
+                      _marketSignAndDropDown(),
+                      SizedBox(height: 15.h),
+                      _tabBar(),
+
+                    ],
+                  ),
+                ),
+              ],
+            ),
+            Positioned(
+                top: 0,
+                left: 0,
+                child: _customizeButton(onTap: controller.clickCustomCharacter)),
+          ],
         ),
-      ),
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        crossAxisAlignment: CrossAxisAlignment.start,
-        mainAxisSize: MainAxisSize.min,
+      ],
+    );
+  }
+  // 人设市场标识和下拉框
+  Widget _marketSignAndDropDown() {
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
         children: [
-          _customizeButton(),
-          SizedBox(height: 14.h),
-          Row(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
-            children: [
-              Assets.images.iconCharacterMarket.image(
-                width: 73.w,
-                height: 25.h,
+          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,
               ),
-              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.currentKeyboardInfo.value?.name,
-                  onChanged: (String? newValue) {
-                    controller.updateSelectedValue(newValue);
-                  },
+              icon: Assets.images.iconCharacterArrowDown.image(
+                width: 20.r,
+                height: 20.r,
+              ),
+              value: controller.currentKeyboardInfo.value.name,
+              onChanged: (String? newValue) {
+                controller.switchKeyboard(newValue);
+              },
 
-                  items: List.generate(controller.keyboardInfoList.length, (
-                    index,
-                  ) {
-                    String? value = controller.keyboardInfoList[index].name;
-                    return DropdownMenuItem<String>(
-                      value: value,
-                      child: Column(
-                        crossAxisAlignment: CrossAxisAlignment.start,
-                        mainAxisSize: MainAxisSize.min,
-                        children: [
-                          Padding(
-                            padding: EdgeInsets.symmetric(vertical: 8),
-                            child: Text(
-                              value ?? "",
-                              style: TextStyle(
-                                color: Colors.black.withAlpha(204),
-                                fontSize: 14.sp,
-                                fontWeight: FontWeight.w400,
-                              ),
-                            ),
+              items: List.generate(
+                controller.keyboardInfoList.length,
+                    (index) {
+                  String? value =
+                  controller.keyboardInfoList[index].name;
+                  return DropdownMenuItem<String>(
+                    value: value,
+                    child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      mainAxisSize: MainAxisSize.min,
+                      children: [
+                        Padding(
+                          padding: EdgeInsets.symmetric(
+                            vertical: 8,
                           ),
-                          if (index != controller.keyboardInfoList.length - 1)
-                            Divider(
-                              color: Color(0xFFF6F6F6),
-                              thickness: 1,
-                              height: 1,
+                          child: Text(
+                            value ?? "",
+                            style: TextStyle(
+                              color: Colors.black.withAlpha(204),
+                              fontSize: 14.sp,
+                              fontWeight: FontWeight.w400,
                             ),
-                        ],
-                      ),
-                    );
-                  }),
-                );
-              }),
-            ],
-          ),
-          SizedBox(height: 15.h),
-          tabBar(),
+                          ),
+                        ),
+                        if (index !=
+                            controller.keyboardInfoList.length - 1)
+                          Divider(
+                            color: Color(0xFFF6F6F6),
+                            thickness: 1,
+                            height: 1,
+                          ),
+                      ],
+                    ),
+                  );
+                },
+              ),
+            );
+          }),
         ],
       ),
     );
   }
 
   // 定制按钮
-  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,
+  Widget _customizeButton({required VoidCallback onTap}) {
+    return GestureDetector(
+      onTap: onTap,
+      child: 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),
           ),
-          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,
+        ),
+        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,
+                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,
+            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() {
+  Widget _tabBar() {
     return Obx(() {
       if (controller.characterGroupList.isEmpty) {
         return const SizedBox.shrink();
@@ -226,44 +260,53 @@ class CharacterView extends BaseView<CharacterController> {
         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,
+          return Column(
+            children: [
+              Container(
+                width: 80.w,
+                height: isSelected ? 38.h : 32.h,
+                padding: isSelected
+                    ?EdgeInsets.only(bottom: 4.h)
+                    :EdgeInsets.zero,
+                decoration:
+                    isSelected
+                        ? BoxDecoration(
+                          borderRadius: BorderRadius.circular(36.r),
+                          image: DecorationImage(
+                            image:
+                                Assets.images.iconCharacterGroupSelected.provider(),
+                            fit: BoxFit.fill,
+                          ),
+                        )
+                        : BoxDecoration(
+                          color: Colors.white.withAlpha(204),
+                          borderRadius: BorderRadius.circular(36.r),
+                        ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    if (e.iconUrl != null)
+                      CachedNetworkImage(
+                        imageUrl: e.iconUrl!,
+                        width: 20.r,
+                        height: 20.r,
                       ),
-                    )
-                    : BoxDecoration(
-                      color: Colors.white.withAlpha(204),
-                      borderRadius: BorderRadius.circular(36.r),
-                    ),
-            child: Row(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: [
-                if (e.iconUrl != null)
-                  CachedNetworkImage(
-                    imageUrl: 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,
-                  ),
+                    Text(
+                      e.name ?? "",
+                      style: TextStyle(
+                        color:
+                            isSelected ? Colors.black : Colors.black.withAlpha(104),
+                        fontSize: 14.sp,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    ),
+                  ],
                 ),
-              ],
-            ),
+              ),
+              !isSelected? SizedBox(height: 4.h):SizedBox(),
+
+            ],
           );
         }),
       );
@@ -287,20 +330,21 @@ class CharacterView extends BaseView<CharacterController> {
       );
     });
   }
+
 }
 
-/// **🔹 让背景图滑动时裁剪掉上方部分**
+/// **🔹 可伸缩的
 class CharacterHeaderDelegate extends SliverPersistentHeaderDelegate {
   final double expandedHeight;
   final double minHeight;
 
-  // final Widget bottomWidget;
+  final Widget bottomWidget;
   final VoidCallback onTap;
 
   CharacterHeaderDelegate({
     required this.expandedHeight,
     required this.minHeight,
-    // required this.bottomWidget,
+    required this.bottomWidget,
     required this.onTap,
   });
 
@@ -314,92 +358,34 @@ class CharacterHeaderDelegate extends SliverPersistentHeaderDelegate {
       minHeight,
       expandedHeight,
     );
-    final tabBarOffset = expandedHeight - currentVisibleHeight; // 计算 TabBar 位移
 
     final opacity = 1 - currentVisibleHeight / expandedHeight;
     return Stack(
-      clipBehavior: Clip.none,
+      // 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,
-            ),
+          child: Image.asset(
+            Assets.images.bgCharacterBoyBanner.path,
+            width: double.infinity,
+            fit: BoxFit.fill,
+            alignment: Alignment.topCenter,
           ),
         ),
-
         // 遮罩层 Positioned(用于控制背景的可见性)
         Positioned(
           top: 0,
           left: 0,
           right: 0,
           height: currentVisibleHeight,
-          child: Container(color: Colors.black.withValues(alpha: opacity)),
+          child: Opacity(opacity: opacity, child: Container(color: Colors.purple.shade700)),
         ),
-        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,
-        //   ),
-        // ),
+        Positioned(bottom: 0, left: 0, right: 0, child: bottomWidget),
+
+        // 我的键盘按钮
+        myKeyboardButton(onTap: onTap),
       ],
     );
   }
@@ -415,28 +401,52 @@ class CharacterHeaderDelegate extends SliverPersistentHeaderDelegate {
       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;
+//我的键盘按钮
+Widget myKeyboardButton({required VoidCallback onTap}) {
+  return 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,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    ),
+  );
 }

+ 48 - 15
lib/module/character/content/character_group_content_controller.dart

@@ -1,17 +1,19 @@
 import 'package:easy_refresh/easy_refresh.dart';
 import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/base/base_controller.dart';
-import 'package:get/get.dart';
 import 'package:keyboard/data/repository/characters_repository.dart';
 import 'package:keyboard/dialog/character_details_dialog.dart';
 import 'package:keyboard/module/character/character_controller.dart';
 import 'package:keyboard/utils/atmob_log.dart';
+import 'package:keyboard/utils/http_handler.dart';
 import 'package:keyboard/utils/toast_util.dart';
 
 import '../../../data/bean/character_group_info.dart';
 import '../../../data/bean/character_info.dart';
 import '../../../data/bean/keyboard_info.dart';
+import '../../../utils/error_handler.dart';
 
 @injectable
 class CharacterGroupContentController extends BaseController {
@@ -99,32 +101,63 @@ class CharacterGroupContentController extends BaseController {
 
   void itemButtonClick(CharacterInfo characterInfo) {
     AtmobLog.d(tag, 'characterInfo ${characterInfo.toJson()} ');
-
     CharacterDetailsDialog.show(
       characterInfo: characterInfo,
       clickCallback: () {
-        if (characterInfo.isLock == true) {
-          // addCharacter(characterInfo);
+        if (characterInfo.isVip == true && characterInfo.isLock == true) {
+          unlockCharacter(characterInfo);
         } else if (characterInfo.isAdd == false) {
-          // unlockCharacter(characterInfo);
-        } else {
-          ToastUtil.show('该人设已添加');
+          addCharacter(characterInfo);
         }
       },
     );
   }
 
   void addCharacter(CharacterInfo characterInfo) {
-    charactersRepository.characterAdd(
-      characterId: characterInfo.id.toString(),
-      keyboardId: currentKeyboardInfo.value.id.toString(),
-    );
+    charactersRepository
+        .characterAdd(
+          characterId: characterInfo.id.toString(),
+          keyboardId: currentKeyboardInfo.value.id.toString(),
+        )
+        .then((characterAddResponse) {
+          int index = characterList.indexWhere(
+            (element) => element.id == characterAddResponse.characterInfo.id,
+          );
+          if (index != -1) {
+            characterList[index] = characterAddResponse.characterInfo;
+          }
+          ToastUtil.show('添加成功~');
+        })
+        .catchError((error) {
+          if (error is ServerErrorException && error.code == 1005) {
+            ToastUtil.show('请开通会员解锁权益~');
+          } else {
+            ErrorHandler.toastError(error);
+          }
+        });
   }
 
   void unlockCharacter(CharacterInfo characterInfo) {
-    charactersRepository.characterUnlock(
-      characterId: characterInfo.id.toString(),
-      keyboardId: currentKeyboardInfo.value.id.toString(),
-    );
+    charactersRepository
+        .characterUnlock(
+          characterId: characterInfo.id.toString(),
+          keyboardId: currentKeyboardInfo.value.id.toString(),
+        )
+        .then((characterUnlockResponse) {
+          int index = characterList.indexWhere(
+            (element) => element.id == characterUnlockResponse.characterInfo.id,
+          );
+          if (index != -1) {
+            characterList[index] = characterUnlockResponse.characterInfo;
+          }
+          ToastUtil.show('解锁成功~');
+        })
+        .catchError((error) {
+          if (error is ServerErrorException && error.code == 1005) {
+            ToastUtil.show('请开通会员解锁权益~');
+          } else {
+            ErrorHandler.toastError(error);
+          }
+        });
   }
 }

+ 21 - 16
lib/module/character/content/character_group_content_view.dart

@@ -59,23 +59,28 @@ class CharacterGroupContentView
   }
 
   Widget _buildListItem({required CharacterInfo characterInfo}) {
-    return Container(
-      margin: EdgeInsets.symmetric(horizontal: 16.w),
-      padding: EdgeInsets.all(14.r),
-      decoration: ShapeDecoration(
-        color: Colors.white,
-        shape: RoundedRectangleBorder(
-          borderRadius: BorderRadius.circular(12.r),
+    return GestureDetector(
+      onTap: () {
+        controller.itemButtonClick(characterInfo);
+      },
+      child: Container(
+        margin: EdgeInsets.symmetric(horizontal: 16.w),
+        padding: EdgeInsets.all(14.r),
+        decoration: ShapeDecoration(
+          color: Colors.white,
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(12.r),
+          ),
+        ),
+        height: 88.h,
+        child: Row(
+          children: [
+            _buildAvatar(imageUrl: characterInfo.imageUrl),
+            SizedBox(width: 8.w),
+            _buildCharacterInfo(characterInfo),
+            _buildActionButton(characterInfo),
+          ],
         ),
-      ),
-      height: 88.h,
-      child: Row(
-        children: [
-          _buildAvatar(imageUrl: characterInfo.imageUrl),
-          SizedBox(width: 8.w),
-          _buildCharacterInfo(characterInfo),
-          _buildActionButton(characterInfo),
-        ],
       ),
     );
   }

+ 20 - 0
lib/module/character_custom/character_custom_controller.dart

@@ -0,0 +1,20 @@
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/base/base_controller.dart';
+import 'package:keyboard/utils/atmob_log.dart';
+import 'package:get/get.dart';
+@injectable
+class CharacterCustomController extends BaseController {
+  final String tag = 'CharacterCustomController';
+
+  CharacterCustomController();
+  var interests = <String>[].obs;
+  clickStartCustom() {
+    AtmobLog.d(tag, "clickStartCustom");
+
+  }
+
+  clickBack() {
+    AtmobLog.d(tag, "clickBack");
+    Get.back();
+  }
+}

+ 306 - 0
lib/module/character_custom/character_custom_page.dart

@@ -0,0 +1,306 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:keyboard/base/base_page.dart';
+import 'package:keyboard/module/character_custom/character_custom_controller.dart';
+import 'package:keyboard/resource/string.gen.dart';
+
+import '../../resource/assets.gen.dart';
+
+class CharacterCustomPage extends BasePage<CharacterCustomController> {
+  const CharacterCustomPage({super.key});
+
+  static start() {
+    return Get.to(() => CharacterCustomPage());
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return buildHappyPage();
+  }
+
+  Widget buildCustomHomePage() {
+    return Stack(
+      children: [
+        Assets.images.bgCharacterCustomHuman.image(
+          width: double.infinity,
+          fit: BoxFit.fill,
+        ),
+        SafeArea(
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Padding(
+                padding: EdgeInsets.only(left: 16.w),
+                child: GestureDetector(
+                  onTap: () {
+                    controller.clickBack();
+                  },
+                  child: Assets.images.iconCharacterCustomClose.image(
+                    width: 24.w,
+                    height: 24.w,
+                  ),
+                ),
+              ),
+              Opacity(
+                opacity: 0.80,
+                child: Container(
+                  width: 76.r,
+                  height: 32.h,
+                  decoration: ShapeDecoration(
+                    color: const Color(0xFF400164),
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.only(
+                        topLeft: Radius.circular(16.r),
+                        bottomLeft: Radius.circular(16.r),
+                      ),
+                    ),
+                  ),
+                  child: Center(
+                    child: Text(
+                      '定制历史',
+                      style: TextStyle(
+                        color: Colors.white,
+                        fontSize: 14.sp,
+
+                        fontWeight: FontWeight.w400,
+                      ),
+                    ),
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+        Positioned(
+          bottom: 50.h,
+          left: 0,
+          right: 0,
+          child: GestureDetector(
+            onTap: () {
+              controller.clickStartCustom();
+            },
+            child: Center(
+              child: Assets.images.iconCharacterCustomButton.image(
+                width: 234.w,
+                fit: BoxFit.contain,
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget buildHappyPage() {
+    return Container(
+      child: Stack(
+        children: [
+          Assets.images.bgCharacterCustomSteps.image(
+            width: double.infinity,
+            fit: BoxFit.fill,
+          ),
+          SafeArea(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Padding(
+                  padding: EdgeInsets.only(left: 16.w),
+                  child: GestureDetector(
+                    onTap: () {
+                      controller.clickBack();
+                    },
+                    child: Assets.images.iconCharacterCustomClose.image(
+                      width: 24.w,
+                      height: 24.w,
+                    ),
+                  ),
+                ),
+                Expanded(
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    crossAxisAlignment: CrossAxisAlignment.center,
+                    children: [
+
+                      Assets.images.iconCharacterCustomStepOneTitle.image(
+                        fit: BoxFit.cover,
+                        width: 214.w,
+                      ),
+                      Container(
+                        margin: EdgeInsets.only(
+                          left: 16.w,
+                          right: 16.w,
+                          top: 24.h,
+                        ),
+                        width: double.infinity,
+                        decoration: ShapeDecoration(
+                          color: Colors.white,
+                          shape: RoundedRectangleBorder(
+                            borderRadius: BorderRadius.circular(20.r),
+                          ),
+                          shadows: [
+                            BoxShadow(
+                              color: Color(0x66D788FF),
+                              blurRadius: 10.r,
+                              offset: Offset(0, 0),
+                              spreadRadius: 0,
+                            ),
+                          ],
+                        ),
+                        child: Column(
+                          children: [
+                            Container(
+                              padding: EdgeInsets.symmetric(horizontal: 26.w),
+                              decoration: BoxDecoration(
+                                image: DecorationImage(
+                                  image:
+                                      Assets.images.bgCharacterCustomStepsDesc
+                                          .provider(),
+                                  fit: BoxFit.fill,
+                                ),
+                              ),
+                              child: Text(
+                                StringName.characterCustomStepsDesc,
+                                style: TextStyle(
+                                  color: const Color(0xFFAD88EB),
+                                  fontSize: 12.sp,
+                                  fontWeight: FontWeight.w400,
+                                ),
+                              ),
+                            ),
+                            SizedBox(height: 24.h),
+                            Row(
+                              mainAxisAlignment: MainAxisAlignment.center,
+                              crossAxisAlignment: CrossAxisAlignment.center,
+                              children: [
+                                Text(
+                                  '第1步',
+                                  style: TextStyle(
+                                    color: const Color(0xFF755AAB),
+                                    fontSize: 12.sp,
+                                    fontWeight: FontWeight.w500,
+                                  ),
+                                ),
+                                Text(
+                                  " | ",
+                                  style: TextStyle(
+                                    color: const Color(0xFF755AAB),
+                                    fontSize: 12.sp,
+                                    fontWeight: FontWeight.w500,
+                                  ),
+                                ),
+                                Opacity(
+                                  opacity: 0.60,
+                                  child: Text(
+                                    '共3步',
+                                    style: TextStyle(
+                                      color: const Color(0xFF755BAB),
+                                      fontSize: 12.sp,
+                                      fontWeight: FontWeight.w500,
+                                    ),
+                                  ),
+                                ),
+                              ],
+                            ),
+                          ],
+                        ),
+                      ),
+                      _buildInterestsPage(),
+                    ],
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+  Widget _buildInterestsPage() {
+    List<Map<String, String>> interestOptions = [
+      {'emoji': '🐱', 'label': '撸猫'},
+      {'emoji': '🐶', 'label': '撸狗'},
+      {'emoji': '📷', 'label': '拍照摄影'},
+      {'emoji': '🔍', 'label': '美食烹饪'},
+      {'emoji': '👗', 'label': '潮流穿搭'},
+      {'emoji': '🎮', 'label': '竞技游戏'},
+      {'emoji': '🎵', 'label': '潮流穿搭潮流穿搭潮流穿搭潮流穿搭'},
+      {'emoji': '✈️', 'label': '旅游'},
+    ];
+
+    return Column(
+      children: [
+        Text('你的兴趣爱好是什么?'),
+        Align(
+          alignment: Alignment.center,
+          child: Wrap(
+            alignment: WrapAlignment.center,
+            spacing: 8.0,
+            runSpacing: 8.0,
+            children: [
+              ...interestOptions.map((item) {
+                String emoji = item['emoji']!;
+                String label = item['label']!;
+                return Obx(() => ChoiceChip(
+                  label: Row(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      Text(emoji, style: TextStyle(fontSize: 18)),
+                      SizedBox(width: 4),
+                      Text(label),
+                    ],
+                  ),
+                  selected: controller.interests.contains(label),
+                  selectedColor: Colors.purple.shade100,
+                  backgroundColor: Colors.white,
+                  shape: RoundedRectangleBorder(
+                    side: BorderSide(color: Colors.purple.shade300),
+                    borderRadius: BorderRadius.circular(20),
+                  ),
+                  onSelected: (selected) {
+                    if (selected && controller.interests.length < 3) {
+                      controller.interests.add(label);
+                    } else if (!selected) {
+                      controller.interests.remove(label);
+                    }
+                  },
+                ));
+              }).toList(),
+              GestureDetector(
+                onTap: () {
+                  // TODO: 处理自定义兴趣的逻辑,例如弹出输入框
+                },
+                child: Container(
+                  padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+                  decoration: BoxDecoration(
+                    border: Border.all(color: Colors.purple.shade300),
+                    borderRadius: BorderRadius.circular(20),
+                  ),
+                  child: Row(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      Icon(Icons.add, size: 18, color: Colors.purple.shade300),
+                      SizedBox(width: 4),
+                      Text('自定义', style: TextStyle(color: Colors.purple.shade300)),
+                    ],
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 503 - 0
lib/module/keyboard_manage/keyboard_manage_controller.dart

@@ -0,0 +1,503 @@
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/base/base_controller.dart';
+import 'package:keyboard/data/bean/character_info.dart';
+import 'package:keyboard/data/repository/keyboard_repository.dart';
+import 'package:keyboard/dialog/character_add_dialog.dart';
+import 'package:keyboard/module/character/content/character_group_content_controller.dart';
+import 'package:keyboard/resource/string.gen.dart';
+import 'package:keyboard/utils/atmob_log.dart';
+import 'package:keyboard/utils/toast_util.dart';
+
+import '../../data/bean/keyboard_info.dart';
+
+enum KeyboardType {
+  system, //通用键盘
+  custom, //自定义键盘
+}
+
+@injectable
+class KeyboardManageController extends BaseController
+    with GetTickerProviderStateMixin {
+  final String tag = 'KeyboardManageController';
+
+  // 自定义键盘列表
+  final RxList<KeyboardInfo> _customKeyboardInfoList = RxList();
+
+  RxList<KeyboardInfo> get customKeyboardInfoList => _customKeyboardInfoList;
+
+  // 当前自定义键盘
+  final Rx<KeyboardInfo> _currentCustomKeyboardInfo = KeyboardInfo().obs;
+
+  Rx<KeyboardInfo> get currentCustomKeyboardInfo => _currentCustomKeyboardInfo;
+
+  //当前自定义键盘人设列表
+  final RxList<CharacterInfo> _currentCustomKeyboardCharacterList = RxList();
+
+  RxList<CharacterInfo> get currentCustomKeyboardCharacterList =>
+      _currentCustomKeyboardCharacterList;
+
+  // 当前自定义键盘亲密度
+  final RxInt _currentCustomIntimacy = 0.obs;
+
+  RxInt get currentCustomIntimacy => _currentCustomIntimacy;
+
+  // 当前定制亲密度是否有变化
+  final RxBool _customIntimacyChanged = false.obs;
+
+  RxBool get customIntimacyChanged => _customIntimacyChanged;
+
+  final RxBool _customKeyboardCharacterListChanged = false.obs;
+
+  RxBool get customKeyboardCharacterListChanged =>
+      _customKeyboardCharacterListChanged;
+
+  // 存储排序前的定制人设列表,用于比较是否有变化
+  late List<CharacterInfo> _oldCustomCharacterList;
+
+  // 通用键盘列表
+  final RxList<KeyboardInfo> _generalKeyboardInfoList = RxList();
+
+  RxList<KeyboardInfo> get generalKeyboardInfoList => _generalKeyboardInfoList;
+
+  // 当前通用键盘
+  final Rx<KeyboardInfo> _currentGeneralKeyboardInfo = KeyboardInfo().obs;
+
+  Rx<KeyboardInfo> get currentGeneralKeyboardInfo =>
+      _currentGeneralKeyboardInfo;
+
+  // 当前通用键盘人设列表
+  final RxList<CharacterInfo> _currentGeneralKeyboardCharacterList = RxList();
+
+  RxList<CharacterInfo> get currentGeneralKeyboardCharacterList =>
+      _currentGeneralKeyboardCharacterList;
+
+  // 当前通用键盘亲密度
+  final RxInt _currentGeneralIntimacy = 0.obs;
+
+  RxInt get currentGeneralIntimacy => _currentGeneralIntimacy;
+
+  // 当前通用亲密度是否有变化
+  final RxBool _generalIntimacyChanged = false.obs;
+
+  RxBool get generalIntimacyChanged => _generalIntimacyChanged;
+
+  final RxBool _generalKeyboardCharacterListChanged = false.obs;
+
+  RxBool get generalKeyboardCharacterListChanged =>
+      _generalKeyboardCharacterListChanged;
+
+  // 存储排序前的通用人设列表,用于比较是否有变化
+  late List<CharacterInfo> _oldGeneralCharacterList;
+
+  final KeyboardRepository keyboardRepository;
+
+  // 最小人设数量
+  final _minCount = 9;
+
+  // 键盘管理类型
+  List<String> keyboardManageType = [
+    StringName.keyboardCustom,
+    StringName.generalKeyboard,
+  ];
+
+  // 键盘管理页面的tabController,用于控制通用键盘和自定义键盘的切换
+  late TabController tabController;
+
+  // 键盘管理页面的pageController,用于控制通用键盘和自定义键盘的切换
+  late PageController pageController;
+
+  KeyboardManageController(this.keyboardRepository);
+
+  @override
+  void onInit() {
+    super.onInit();
+    _dataLoad();
+  }
+
+  _dataLoad() {
+    tabController = TabController(
+      length: keyboardManageType.length,
+      vsync: this,
+      initialIndex: 0,
+    );
+    tabController.addListener(() {
+      if (tabController.indexIsChanging) {
+        switchTabKeyboardType(tabController.index);
+      }
+    });
+
+    pageController = PageController();
+
+    getCustomKeyboard();
+    getGeneralKeyboard();
+  }
+
+  clickBack() {
+    AtmobLog.i(tag, 'clickBack');
+    Get.back();
+  }
+
+  //   获取定制键盘
+  void getCustomKeyboard() {
+    AtmobLog.i(tag, 'getCustomKeyboard');
+    keyboardRepository.getKeyboardList(type: KeyboardType.custom.name).then((
+      keyboardListResponse,
+    ) {
+      AtmobLog.i(
+        tag,
+        'keyboardListResponse: ${keyboardListResponse.keyboardInfos}',
+      );
+      _customKeyboardInfoList.value = keyboardListResponse.keyboardInfos;
+
+      //检查是否是选择的键盘,如果没有选择的键盘,默认选择第一个
+      if (_customKeyboardInfoList.isNotEmpty) {
+        _currentCustomKeyboardInfo.value = _customKeyboardInfoList.firstWhere(
+          (element) => element.isChoose == true,
+          orElse: () => _customKeyboardInfoList.first,
+        );
+        _currentCustomIntimacy.value =
+            _currentCustomKeyboardInfo.value.intimacy ?? 0;
+        _currentCustomIntimacy.listen((intimacy) {
+          _customIntimacyChanged.value =
+              _currentCustomKeyboardInfo.value.intimacy != intimacy;
+          AtmobLog.d(tag, 'intimacyChanged: $_customIntimacyChanged');
+        });
+
+        String? id = _currentCustomKeyboardInfo.value.id;
+        if (id != null) {
+          getKeyboardCharacterList(keyboardId: id, isCustom: true);
+        }
+      }
+    });
+  }
+
+  //   获取通用键盘
+  void getGeneralKeyboard() {
+    AtmobLog.i(tag, 'getGeneralKeyboard');
+    keyboardRepository.getKeyboardList(type: KeyboardType.system.name).then((
+      keyboardListResponse,
+    ) {
+      AtmobLog.i(
+        tag,
+        'keyboardListResponse: ${keyboardListResponse.keyboardInfos}',
+      );
+      _generalKeyboardInfoList.value = keyboardListResponse.keyboardInfos;
+      _currentGeneralKeyboardInfo.value = _generalKeyboardInfoList.first;
+      _currentGeneralIntimacy.value =
+          _currentGeneralKeyboardInfo.value.intimacy ?? 0;
+      _currentGeneralIntimacy.listen((intimacy) {
+        _generalIntimacyChanged.value =
+            _currentGeneralKeyboardInfo.value.intimacy != intimacy;
+        AtmobLog.d(tag, 'intimacyChanged: $_generalIntimacyChanged');
+      });
+
+      String? id = _currentGeneralKeyboardInfo.value.id;
+      if (id != null) {
+        getKeyboardCharacterList(keyboardId: id, isCustom: false);
+      }
+    });
+  }
+
+  // 获取当前键盘人设列表
+  void getKeyboardCharacterList({
+    required String keyboardId,
+    required bool isCustom,
+  }) {
+    if (isCustom) {
+      keyboardRepository.getKeyboardCharacterList(keyboardId: keyboardId).then((
+        keyboardCharacterListResponse,
+      ) {
+        AtmobLog.i(
+          tag,
+          'keyboardCharacterListResponse: ${keyboardCharacterListResponse.characterInfos.toString()}',
+        );
+        _currentCustomKeyboardCharacterList.value =
+            keyboardCharacterListResponse.characterInfos;
+        _oldCustomCharacterList = List<CharacterInfo>.from(
+          _currentCustomKeyboardCharacterList,
+        );
+        _customKeyboardCharacterListChanged.value = false;
+        _currentCustomKeyboardCharacterList.listen((event) {
+          _customKeyboardCharacterListChanged.value =
+              !ListEquality().equals(_oldCustomCharacterList, event);
+          AtmobLog.d(
+            tag,
+            '_customKeyboardCharacterListChanged: $_customKeyboardCharacterListChanged',
+          );
+        });
+      });
+    } else {
+      keyboardRepository.getKeyboardCharacterList(keyboardId: keyboardId).then((
+        keyboardCharacterListResponse,
+      ) {
+        AtmobLog.i(
+          tag,
+          'keyboardCharacterListResponse: ${keyboardCharacterListResponse.characterInfos.toString()}',
+        );
+        if (_currentGeneralKeyboardInfo.value.id == keyboardId) {
+          _currentGeneralKeyboardCharacterList.value =
+              keyboardCharacterListResponse.characterInfos;
+          _oldGeneralCharacterList = List<CharacterInfo>.from(
+            _currentGeneralKeyboardCharacterList,
+          );
+          _generalKeyboardCharacterListChanged.value = false;
+          _currentGeneralKeyboardCharacterList.listen((event) {
+            _generalKeyboardCharacterListChanged.value =
+                !ListEquality().equals(_oldGeneralCharacterList, event);
+            AtmobLog.d(
+              tag,
+              '_generalKeyboardCharacterListChanged: $_generalKeyboardCharacterListChanged',
+            );
+          });
+        }
+        if (_currentCustomKeyboardInfo.value.id == keyboardId) {
+          _currentCustomKeyboardCharacterList.value =
+              keyboardCharacterListResponse.characterInfos;
+        }
+      });
+    }
+  }
+
+  //   切换当前定制键盘
+  void switchCustomKeyboard(String? keyboardName) {
+    _currentCustomKeyboardInfo.value = _customKeyboardInfoList.firstWhere(
+      (element) => element.name == keyboardName,
+      orElse: () => _customKeyboardInfoList.first,
+    );
+    String? keyboardId = _currentCustomKeyboardInfo.value.id;
+    _currentCustomIntimacy.value =
+        _currentCustomKeyboardInfo.value.intimacy ?? 0;
+    if (keyboardId != null) {
+      getKeyboardCharacterList(keyboardId: keyboardId, isCustom: true);
+    }
+  }
+
+  //   切换当前通用键盘
+  void switchGeneralKeyboard(String? keyboardName) {
+    _currentGeneralKeyboardInfo.value = _generalKeyboardInfoList.firstWhere(
+      (element) => element.name == keyboardName,
+      orElse: () => _generalKeyboardInfoList.first,
+    );
+    String? keyboardId = _currentGeneralKeyboardInfo.value.id;
+    _currentGeneralIntimacy.value =
+        _currentGeneralKeyboardInfo.value.intimacy ?? 0;
+    if (keyboardId != null) {
+      getKeyboardCharacterList(keyboardId: keyboardId, isCustom: false);
+    }
+  }
+
+  // tab切换
+  void switchTabKeyboardType(int index) {
+    // AtmobLog.i(tag, 'onTabChanged: $index');
+    pageController.animateToPage(
+      index,
+      duration: const Duration(milliseconds: 300),
+      curve: Curves.easeInToLinear,
+    );
+  }
+
+  // page切换
+  void switchPageKeyboardType(int index) {
+    // AtmobLog.i(tag, 'onPageChanged: $index');
+
+    tabController.animateTo(index, duration: const Duration(milliseconds: 300));
+
+    if (index == 0) {
+      _currentGeneralIntimacy.value =
+          _currentGeneralKeyboardInfo.value.intimacy ?? 0;
+      _currentGeneralKeyboardCharacterList.value = _oldGeneralCharacterList;
+      _generalIntimacyChanged.value = false;
+      _generalKeyboardCharacterListChanged.value = false;
+      getCustomKeyboard();
+    } else {
+      _currentCustomIntimacy.value =
+          _currentCustomKeyboardInfo.value.intimacy ?? 0;
+      _currentCustomKeyboardCharacterList.value = _oldCustomCharacterList;
+      _customIntimacyChanged.value = false;
+      _customKeyboardCharacterListChanged.value = false;
+      getGeneralKeyboard();
+    }
+  }
+
+  // 更新亲密度
+  void updateIntimacy(int intimacy, bool isCustom) {
+    if (isCustom) {
+      _currentCustomIntimacy.value = intimacy;
+    } else {
+      _currentGeneralIntimacy.value = intimacy;
+    }
+  }
+
+  // 排序
+  void onReorder(int oldIndex, int newIndex, bool isCustom) {
+    if (isCustom) {
+      reorderList(_currentCustomKeyboardCharacterList, oldIndex, newIndex);
+    } else {
+      reorderList(_currentGeneralKeyboardCharacterList, oldIndex, newIndex);
+    }
+  }
+
+  // 排序
+  void reorderList<T>(RxList<T> list, int oldIndex, int newIndex) {
+    AtmobLog.d(tag, 'reorderList: $oldIndex, $newIndex');
+    final item = list.removeAt(oldIndex);
+    list.insert(newIndex, item);
+  }
+
+  void clickSave(bool isCustom) {
+    isCustom
+        ? saveCustomKeyboardCharacterList()
+        : saveGeneralKeyboardCharacterList();
+
+  }
+
+  void saveCustomKeyboardCharacterList() {
+    if (_customIntimacyChanged.value) {
+      AtmobLog.i(tag, 'clickSave intimacyChanged');
+      _currentCustomKeyboardInfo.value.intimacy = currentCustomIntimacy.value;
+      String? keyboardId = _currentCustomKeyboardInfo.value.id;
+      if (keyboardId != null) {
+        keyboardRepository
+            .updateKeyboardInfo(
+              keyboardId: keyboardId,
+              intimacy: currentCustomIntimacy.value,
+            )
+            .then((value) {
+              ToastUtil.show(StringName.keyboardSaveSuccess);
+
+              CharacterGroupContentController characterGroupContentController =
+              Get.find<CharacterGroupContentController>();
+              characterGroupContentController.refreshData();
+            })
+            .catchError((error) {
+              ToastUtil.show(StringName.keyboardSaveFailed);
+              _customIntimacyChanged.value = false;
+            });
+      }
+    }
+    if (_customKeyboardCharacterListChanged.value) {
+      AtmobLog.i(tag, 'clickSave keyboardChanged');
+      String? keyboardId = _currentCustomKeyboardInfo.value.id;
+      if (keyboardId != null) {
+        List<String> characterIds =
+            _currentCustomKeyboardCharacterList
+                .map((e) => e.id)
+                .toList()
+                .cast<String>();
+        keyboardRepository
+            .keyboardCharacterUpdate(
+              characterIds: characterIds,
+              keyboardId: keyboardId,
+            )
+            .then((value) {
+              _oldCustomCharacterList = List<CharacterInfo>.from(
+                _currentCustomKeyboardCharacterList,
+              );
+
+              CharacterGroupContentController characterGroupContentController =
+              Get.find<CharacterGroupContentController>();
+              characterGroupContentController.refreshData();
+              ToastUtil.show(StringName.keyboardSaveSuccess);
+            })
+            .catchError((error) {
+              ToastUtil.show(StringName.keyboardSaveFailed);
+              _customKeyboardCharacterListChanged.value = false;
+            });
+      }
+    }
+  }
+
+  void saveGeneralKeyboardCharacterList() {
+    if (_generalIntimacyChanged.value) {
+      AtmobLog.i(tag, 'clickSave intimacyChanged');
+      _currentGeneralKeyboardInfo.value.intimacy = currentGeneralIntimacy.value;
+      String? keyboardId = _currentGeneralKeyboardInfo.value.id;
+      if (keyboardId != null) {
+        keyboardRepository
+            .updateKeyboardInfo(
+              keyboardId: keyboardId,
+              intimacy: currentGeneralIntimacy.value,
+            )
+            .then((value) {
+              ToastUtil.show(StringName.keyboardSaveSuccess);
+            })
+            .catchError((error) {
+              ToastUtil.show(StringName.keyboardSaveFailed);
+              _generalIntimacyChanged.value = false;
+            });
+      }
+    }
+    if (_generalKeyboardCharacterListChanged.value) {
+      AtmobLog.i(tag, 'clickSave keyboardChanged');
+      String? keyboardId = _currentGeneralKeyboardInfo.value.id;
+      if (keyboardId != null) {
+        List<String> characterIds =
+            _currentGeneralKeyboardCharacterList
+                .map((e) => e.id)
+                .toList()
+                .cast<String>();
+        keyboardRepository
+            .keyboardCharacterUpdate(
+              characterIds: characterIds,
+              keyboardId: keyboardId,
+            )
+            .then((value) {
+              _oldGeneralCharacterList = List<CharacterInfo>.from(
+                _currentGeneralKeyboardCharacterList,
+              );
+              ToastUtil.show(StringName.keyboardSaveSuccess);
+            })
+            .catchError((error) {
+              ToastUtil.show(StringName.keyboardSaveFailed);
+              _generalKeyboardCharacterListChanged.value = false;
+            });
+      }
+    }
+  }
+
+  void clickRemoveCharacter(CharacterInfo characterInfo, bool isCustom) {
+    if (isCustom) {
+      if (_currentCustomKeyboardCharacterList.length <= _minCount) {
+        ToastUtil.show("最少需要保持$_minCount个人设");
+        return;
+      }
+      AtmobLog.i(tag, 'clickRemoveCharacter');
+      _currentCustomKeyboardCharacterList.remove(characterInfo);
+    } else {
+      if (_currentGeneralKeyboardCharacterList.length <= _minCount) {
+        ToastUtil.show("最少需要保持$_minCount个人设");
+        return;
+      }
+      AtmobLog.i(tag, 'clickRemoveCharacter');
+
+      _currentGeneralKeyboardCharacterList.remove(characterInfo);
+    }
+  }
+
+  clickAddCharacter({required bool isCustom}) {
+    if (isCustom) {
+      AtmobLog.i(tag, 'clickAddCharacter');
+      CharacterAddDialog.show(
+        clickCallback: () {
+          AtmobLog.i(tag, 'clickAddCharacter');
+        },
+      );
+    } else {
+      AtmobLog.i(tag, 'clickAddCharacter');
+    }
+  }
+
+  clickCustomCharacter() {
+    AtmobLog.i(tag, 'clickCustomCharacter');
+  }
+
+  @override
+  void onClose() {
+    tabController.dispose();
+    pageController.dispose();
+    super.onClose();
+  }
+}

+ 545 - 0
lib/module/keyboard_manage/keyboard_manage_page.dart

@@ -0,0 +1,545 @@
+import 'package:dotted_border/dotted_border.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:keyboard/base/base_page.dart';
+import 'package:keyboard/module/keyboard_manage/keyboard_manage_controller.dart';
+import 'package:keyboard/resource/string.gen.dart';
+import 'package:reorderables/reorderables.dart';
+
+import '../../data/bean/character_info.dart';
+import '../../resource/assets.gen.dart';
+import '../../router/app_pages.dart';
+import '../../widget/gradient_rect_slider_track_shape.dart';
+import '../../widget/tab_custom_gradient_indicator.dart';
+
+class KeyboardManagePage extends BasePage<KeyboardManageController> {
+  const KeyboardManagePage({super.key});
+
+  @override
+  immersive() {
+    return true;
+  }
+
+  static start() {
+    Get.toNamed(RoutePath.keyboardManage);
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        image: DecorationImage(
+          image: Assets.images.bgKeyboardManage.provider(),
+          fit: BoxFit.fill,
+        ),
+      ),
+      child: SafeArea(
+        child: Container(
+          alignment: Alignment.topCenter,
+          child: Column(
+            children: [
+              // TabBar
+              buildTitle(),
+              SizedBox(height: 10.h),
+              Expanded(
+                child: Container(
+                  decoration: ShapeDecoration(
+                    color: Colors.white,
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.only(
+                        topLeft: Radius.circular(20.r),
+                        topRight: Radius.circular(20.r),
+                      ),
+                    ),
+                  ),
+                  child: PageView(
+                    controller: controller.pageController,
+                    onPageChanged: (index) {
+                      controller.switchPageKeyboardType(index);
+                    },
+                    children: [
+                      _buildCustomKeyboardSettings(),
+                      _buildGeneralKeyboardSettings(),
+                    ],
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget buildTitle() {
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      child: Row(
+        children: [
+          GestureDetector(
+            onTap: controller.clickBack,
+            child: Assets.images.iconMineBackArrow.image(
+              width: 24.w,
+              height: 24.w,
+            ),
+          ),
+          Expanded(
+            child: TabBar(
+              // onTap: controller.switchTabKeyboardType,
+              controller: controller.tabController,
+              tabs:
+                  controller.keyboardManageType
+                      .map((e) => Tab(text: e))
+                      .toList(),
+              dividerHeight: 0,
+              indicator: TabCustomGradientIndicator(),
+              labelStyle: TextStyle(
+                color: Colors.black.withAlpha(204),
+                fontSize: 17.sp,
+                fontWeight: FontWeight.w500,
+              ),
+
+              unselectedLabelStyle: TextStyle(
+                color: Colors.black.withAlpha(102),
+                fontSize: 17.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            ),
+          ),
+          SizedBox(width: 24.w),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildCustomKeyboardSettings() {
+    return Column(
+      children: [
+        Expanded(
+          child: SingleChildScrollView(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                _buildDropdownButton(), // 下拉框
+                _buildIntimacySlider(isCustom: true), // 亲密度模块
+                _buildKeyboardCharacter(isCustom: true), // 键盘人设
+              ],
+            ),
+          ),
+        ),
+        _buildSaveButton(isCustom: true),
+      ],
+    );
+  }
+
+  Widget _buildGeneralKeyboardSettings() {
+    return Column(
+      children: [
+        Expanded(
+          child: SingleChildScrollView(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                _buildIntimacySlider(isCustom: false), // 亲密度模块
+                _buildKeyboardCharacter(isCustom: false), // 键盘人设
+              ],
+            ),
+          ),
+        ),
+        _buildSaveButton(isCustom: false),
+      ],
+    );
+  }
+
+  Widget _buildDropdownButton() {
+    return Obx(() {
+      return Padding(
+        padding: EdgeInsets.only(left: 16.w, top: 24.h, right: 16.w),
+        child: Row(children: [
+          Text("自己&",
+              textAlign: TextAlign.center,
+              style: TextStyle(
+            color: Colors.black.withAlpha(204),
+            fontSize: 16.sp,
+            fontWeight: FontWeight.w500,
+          )),
+          DropdownButton<String>(
+            underline: Container(height: 0),
+            style: TextStyle(
+              color: Colors.black.withAlpha(204),
+              fontSize: 16.sp,
+              fontWeight: FontWeight.w500,
+            ),
+            icon: Assets.images.iconCharacterArrowDown.image(
+              width: 20.r,
+              height: 20.r,
+            ),
+            value: controller.currentCustomKeyboardInfo.value.name,
+            onChanged: (String? newValue) {
+              controller.switchCustomKeyboard(newValue);
+            },
+
+            items: List.generate(controller.customKeyboardInfoList.length, (
+                index,
+                ) {
+              String? value = controller.customKeyboardInfoList[index].name;
+              return DropdownMenuItem<String>(
+                value: value,
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    Padding(
+                      padding: EdgeInsets.symmetric(vertical: 8),
+                      child: Text(
+                        value ?? "",
+                        style: TextStyle(
+                          color: Colors.black.withAlpha(204),
+                          fontSize: 16.sp,
+                          fontWeight: FontWeight.w500,
+                        ),
+                      ),
+                    ),
+                    if (index != controller.customKeyboardInfoList.length - 1)
+                      Divider(color: Color(0xFFF6F6F6), thickness: 1, height: 1),
+                  ],
+                ),
+              );
+            }),
+          ),
+        ],)
+      );
+    });
+  }
+
+  Widget _buildIntimacySlider({required bool isCustom}) {
+    return // 亲密度模块
+    Container(
+      margin: EdgeInsets.only(left: 16.w, top: 24.h, right: 16.w),
+      padding: EdgeInsets.only(
+        left: 16.w,
+        top: 23.h,
+        right: 16.w,
+        bottom: 26.h,
+      ),
+      decoration: BoxDecoration(
+        image: DecorationImage(
+          image: Assets.images.bgKeyboardManageIntimacy.provider(),
+          fit: BoxFit.fill,
+        ),
+        borderRadius: BorderRadius.circular(10.r),
+      ),
+      child: Column(
+        children: [
+          // 亲密度
+          Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              Assets.images.iconKeyboardManageFavorite.image(
+                width: 20.w,
+                height: 20.w,
+              ),
+              Assets.images.iconKeyboardManageIntimacyText.image(
+                width: 48.w,
+                height: 19.h,
+              ),
+              const Spacer(),
+              Container(
+                alignment: Alignment.center,
+                width: 81.w,
+                height: 28.h,
+                decoration: ShapeDecoration(
+                  color: const Color(0xFFE1E0E7),
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(16.r),
+                  ),
+                ),
+                child: Obx(() {
+                  return Text(
+                    isCustom
+                        ? '${StringName.intimacy}${controller.currentCustomIntimacy.value}%'
+                        : '${StringName.intimacy}${controller.currentGeneralIntimacy.value}%',
+                    textAlign: TextAlign.right,
+                    style: TextStyle(
+                      color: Colors.black.withAlpha(204),
+                      fontSize: 12.sp,
+                      fontWeight: FontWeight.w400,
+                    ),
+                  );
+                }),
+              ),
+            ],
+          ),
+          SizedBox(height: 19.h),
+          Builder(
+            builder: (context) {
+              return SliderTheme(
+                data: SliderTheme.of(context).copyWith(
+                  trackShape: const GradientRectSliderTrackShape(),
+                  trackHeight: 8.h,
+                  thumbColor: Colors.white,
+                  thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7.r),
+                  overlayShape: const RoundSliderOverlayShape(
+                    overlayRadius: 16,
+                  ),
+                ),
+                child: Obx(() {
+                  return Slider(
+                    value:
+                        isCustom
+                            ? controller.currentCustomIntimacy.value.toDouble()
+                            : controller.currentGeneralIntimacy.value
+                                .toDouble(),
+
+                    divisions: 100,
+                    min: 0,
+                    max: 100,
+                    onChanged: (value) {
+                      controller.updateIntimacy(value.toInt(), isCustom);
+                    },
+                  );
+                }),
+              );
+            },
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 键盘人设列表
+  Widget _buildKeyboardCharacter({required bool isCustom}) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Padding(
+          padding: EdgeInsets.only(left: 16.w, top: 24.h, bottom: 16.h),
+          child: Text(
+            StringName.keyboardCharacter,
+            style: TextStyle(
+              color: Colors.black.withAlpha(204),
+              fontSize: 14.sp,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+        ),
+
+        Obx(() {
+          return ReorderableWrap(
+            runSpacing: 16.h,
+            spacing: 4.w,
+            runAlignment: WrapAlignment.start,
+            alignment: WrapAlignment.start,
+            needsLongPressDraggable: true,
+            padding: EdgeInsets.symmetric(horizontal: 16.w),
+            onReorder: (oldIndex, newIndex) {
+              controller.onReorder(oldIndex, newIndex, isCustom);
+            },
+            onNoReorder: (int index) {},
+            onReorderStarted: (int index) {},
+            scrollPhysics: const BouncingScrollPhysics(),
+            footer: [
+              _buildFooterItem(
+                image: Assets.images.iconKeyboardManagePlus.image(
+                  width: 18.w,
+                  height: 18.w,
+                ),
+                name: StringName.addCharacter,
+                onTap: () {
+                  controller.clickAddCharacter( isCustom: isCustom);
+                },
+              ),
+              _buildFooterItem(
+                image: Assets.images.iconKeyboardManageCustom.image(
+                  width: 18.w,
+                  height: 18.w,
+                ),
+                name: StringName.customCharacter,
+                onTap: () {
+                  controller.clickCustomCharacter();
+                },
+              ),
+            ],
+
+            children:
+                isCustom
+                    ? controller.currentCustomKeyboardCharacterList
+                        .map(
+                          (e) => _buildKeyboardCharacterItem(
+                            e,
+                            isCustom: isCustom,
+                          ),
+                        )
+                        .toList()
+                    : controller.currentGeneralKeyboardCharacterList
+                        .map(
+                          (e) => _buildKeyboardCharacterItem(
+                            e,
+                            isCustom: isCustom,
+                          ),
+                        )
+                        .toList(),
+          );
+        }),
+      ],
+    );
+  }
+
+  Widget _buildKeyboardCharacterItem(
+    CharacterInfo characterInfo, {
+    required bool isCustom,
+  }) {
+    return SizedBox(
+      width: (Get.width - 40.w) / 3,
+      child: Stack(
+        children: [
+          Container(
+            height: 44.h,
+            margin: EdgeInsets.only(top: 4.h),
+            decoration: ShapeDecoration(
+              color: const Color(0xFFF5F4F9),
+              shape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(8.r),
+              ),
+            ),
+            alignment: Alignment.center,
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                Expanded(
+                  child: Text(
+                    '${characterInfo.emoji}${characterInfo.name}',
+                    style: TextStyle(
+                      color: Colors.black.withAlpha(204),
+                      fontSize: 14.sp,
+                      fontWeight: FontWeight.w400,
+                    ),
+                    textAlign: TextAlign.center,
+                    maxLines: 1,
+                    overflow: TextOverflow.ellipsis,
+                  ),
+                ),
+              ],
+            ),
+          ),
+          Positioned(
+            right: 0,
+            top: 0,
+
+            child: GestureDetector(
+              onTap: () {
+                controller.clickRemoveCharacter(characterInfo, isCustom);
+              },
+              child: Assets.images.iconKeyboardManageX.image(
+                width: 18.w,
+                height: 18.w,
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 添加人设按钮
+  Widget _buildFooterItem({required Widget image, required String name,void Function()? onTap}) {
+    return GestureDetector(onTap: onTap,
+        child: Container(
+      margin: EdgeInsets.only(top: 4.h),
+      child: DottedBorder(
+        color: const Color(0xFFC9C2DB),
+        // 虚线颜色
+        strokeWidth: 1.0.w,
+        // 线条宽度
+        borderType: BorderType.Rect,
+        // 圆角矩形
+        radius: Radius.circular(8.r),
+        // 圆角半径
+        child: Container(
+          width: 102.w,
+          height: 38.h,
+          alignment: Alignment.center,
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              image,
+              Text(
+                name,
+                style: TextStyle(
+                  color: const Color(0xFFC9C2DB),
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    ));
+  }
+
+  //   保存按钮
+  Widget _buildSaveButton({required bool isCustom}) {
+    return Obx(() {
+      bool hasChanges =
+          isCustom
+              ? (controller.customKeyboardCharacterListChanged.value ||
+                  controller.customIntimacyChanged.value)
+              : (controller.generalKeyboardCharacterListChanged.value ||
+                  controller.generalIntimacyChanged.value);
+      return GestureDetector(
+        onTap: () {
+          controller.clickSave(isCustom);
+        },
+        child: Container(
+          width: double.infinity,
+          margin: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.h),
+          height: 48.h,
+          alignment: Alignment.center,
+          decoration:
+              hasChanges
+                  ? ShapeDecoration(
+                    gradient: LinearGradient(
+                      begin: Alignment(0.04, 0.21),
+                      end: Alignment(0.98, 0.76),
+                      colors: [
+                        const Color(0xFF7D46FC),
+                        const Color(0xFFBC87FF),
+                      ],
+                    ),
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(50.r),
+                    ),
+                    shadows: [
+                      BoxShadow(
+                        color: Color(0x66BDA8C9),
+                        blurRadius: 10,
+                        offset: Offset(0, 4),
+                        spreadRadius: 0,
+                      ),
+                    ],
+                  )
+                  : ShapeDecoration(
+                    color: const Color(0x33121212),
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(29.50),
+                    ),
+                  ),
+          child: Text(
+            StringName.keyboardSave,
+            textAlign: TextAlign.center,
+            style: TextStyle(
+              color: Colors.white,
+              fontSize: 16.sp,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+        ),
+      );
+    });
+  }
+}

+ 2 - 2
lib/plugins/keyboard_android_platform.dart

@@ -7,8 +7,8 @@ import 'package:keyboard/data/repository/chat_repository.dart';
 import 'package:keyboard/utils/atmob_log.dart';
 import 'package:keyboard_android/keyboard_android.dart';
 
-import '../data/bean/stream_deepseek_data.dart' as deepseek_data;
-import '../di/get_it.dart';
+import '../../data/bean/stream_deepseek_data.dart' as deepseek_data;
+import '../../di/get_it.dart';
 
 @lazySingleton
 class KeyboardAndroidPlatform {

+ 84 - 0
lib/resource/assets.gen.dart

@@ -16,6 +16,18 @@ class $AssetsImagesGen {
   AssetGenImage get bgCharacterBoyBanner =>
       const AssetGenImage('assets/images/bg_character_boy_banner.webp');
 
+  /// File path: assets/images/bg_character_custom_human.webp
+  AssetGenImage get bgCharacterCustomHuman =>
+      const AssetGenImage('assets/images/bg_character_custom_human.webp');
+
+  /// File path: assets/images/bg_character_custom_steps.webp
+  AssetGenImage get bgCharacterCustomSteps =>
+      const AssetGenImage('assets/images/bg_character_custom_steps.webp');
+
+  /// File path: assets/images/bg_character_custom_steps_desc.webp
+  AssetGenImage get bgCharacterCustomStepsDesc =>
+      const AssetGenImage('assets/images/bg_character_custom_steps_desc.webp');
+
   /// File path: assets/images/bg_character_dialog.webp
   AssetGenImage get bgCharacterDialog =>
       const AssetGenImage('assets/images/bg_character_dialog.webp');
@@ -28,6 +40,14 @@ class $AssetsImagesGen {
   AssetGenImage get bgCharacterGirlBanner =>
       const AssetGenImage('assets/images/bg_character_girl_banner.webp');
 
+  /// File path: assets/images/bg_keyboard_manage.webp
+  AssetGenImage get bgKeyboardManage =>
+      const AssetGenImage('assets/images/bg_keyboard_manage.webp');
+
+  /// File path: assets/images/bg_keyboard_manage_intimacy.webp
+  AssetGenImage get bgKeyboardManageIntimacy =>
+      const AssetGenImage('assets/images/bg_keyboard_manage_intimacy.webp');
+
   /// File path: assets/images/bg_mine.webp
   AssetGenImage get bgMine => const AssetGenImage('assets/images/bg_mine.webp');
 
@@ -51,6 +71,29 @@ class $AssetsImagesGen {
   AssetGenImage get iconCharacterArrowRight =>
       const AssetGenImage('assets/images/icon_character_arrow_right.webp');
 
+  /// File path: assets/images/icon_character_custom_button.webp
+  AssetGenImage get iconCharacterCustomButton =>
+      const AssetGenImage('assets/images/icon_character_custom_button.webp');
+
+  /// File path: assets/images/icon_character_custom_close.webp
+  AssetGenImage get iconCharacterCustomClose =>
+      const AssetGenImage('assets/images/icon_character_custom_close.webp');
+
+  /// File path: assets/images/icon_character_custom_step_one_title.webp
+  AssetGenImage get iconCharacterCustomStepOneTitle => const AssetGenImage(
+    'assets/images/icon_character_custom_step_one_title.webp',
+  );
+
+  /// File path: assets/images/icon_character_custom_step_three_title.webp
+  AssetGenImage get iconCharacterCustomStepThreeTitle => const AssetGenImage(
+    'assets/images/icon_character_custom_step_three_title.webp',
+  );
+
+  /// File path: assets/images/icon_character_custom_step_two_title.webp
+  AssetGenImage get iconCharacterCustomStepTwoTitle => const AssetGenImage(
+    'assets/images/icon_character_custom_step_two_title.webp',
+  );
+
   /// File path: assets/images/icon_character_customized.webp
   AssetGenImage get iconCharacterCustomized =>
       const AssetGenImage('assets/images/icon_character_customized.webp');
@@ -83,6 +126,31 @@ class $AssetsImagesGen {
   AssetGenImage get iconCharacterVip =>
       const AssetGenImage('assets/images/icon_character_vip.webp');
 
+  /// File path: assets/images/icon_dialog_close_black.webp
+  AssetGenImage get iconDialogCloseBlack =>
+      const AssetGenImage('assets/images/icon_dialog_close_black.webp');
+
+  /// File path: assets/images/icon_keyboard_manage_custom.webp
+  AssetGenImage get iconKeyboardManageCustom =>
+      const AssetGenImage('assets/images/icon_keyboard_manage_custom.webp');
+
+  /// File path: assets/images/icon_keyboard_manage_favorite.webp
+  AssetGenImage get iconKeyboardManageFavorite =>
+      const AssetGenImage('assets/images/icon_keyboard_manage_favorite.webp');
+
+  /// File path: assets/images/icon_keyboard_manage_intimacy_text.webp
+  AssetGenImage get iconKeyboardManageIntimacyText => const AssetGenImage(
+    'assets/images/icon_keyboard_manage_intimacy_text.webp',
+  );
+
+  /// File path: assets/images/icon_keyboard_manage_plus.webp
+  AssetGenImage get iconKeyboardManagePlus =>
+      const AssetGenImage('assets/images/icon_keyboard_manage_plus.webp');
+
+  /// File path: assets/images/icon_keyboard_manage_x.webp
+  AssetGenImage get iconKeyboardManageX =>
+      const AssetGenImage('assets/images/icon_keyboard_manage_x.webp');
+
   /// File path: assets/images/icon_mine_about.webp
   AssetGenImage get iconMineAbout =>
       const AssetGenImage('assets/images/icon_mine_about.webp');
@@ -167,15 +235,25 @@ class $AssetsImagesGen {
   /// List of all assets
   List<AssetGenImage> get values => [
     bgCharacterBoyBanner,
+    bgCharacterCustomHuman,
+    bgCharacterCustomSteps,
+    bgCharacterCustomStepsDesc,
     bgCharacterDialog,
     bgCharacterDialogImage,
     bgCharacterGirlBanner,
+    bgKeyboardManage,
+    bgKeyboardManageIntimacy,
     bgMine,
     bgMineVipCard,
     iconAboutArrowLeft,
     iconBlackBack,
     iconCharacterArrowDown,
     iconCharacterArrowRight,
+    iconCharacterCustomButton,
+    iconCharacterCustomClose,
+    iconCharacterCustomStepOneTitle,
+    iconCharacterCustomStepThreeTitle,
+    iconCharacterCustomStepTwoTitle,
     iconCharacterCustomized,
     iconCharacterDialogClose,
     iconCharacterDialogLogo,
@@ -184,6 +262,12 @@ class $AssetsImagesGen {
     iconCharacterLock,
     iconCharacterMarket,
     iconCharacterVip,
+    iconDialogCloseBlack,
+    iconKeyboardManageCustom,
+    iconKeyboardManageFavorite,
+    iconKeyboardManageIntimacyText,
+    iconKeyboardManagePlus,
+    iconKeyboardManageX,
     iconMineAbout,
     iconMineArrow,
     iconMineBackArrow,

+ 20 - 0
lib/resource/string.gen.dart

@@ -62,6 +62,16 @@ class StringName {
   static final String loadCompleted = 'load_completed'.tr; // 加载完成
   static final String addToKeyboard = 'add_to_keyboard'.tr; // 添加到键盘
   static final String addedToKeyboard = 'added_to_keyboard'.tr; // 已添加到键盘
+  static final String keyboardCustom = 'keyboard_custom'.tr; // 定制键盘
+  static final String generalKeyboard = 'general_keyboard'.tr; // 通用键盘
+  static final String intimacy = 'intimacy'.tr; // 亲密度
+  static final String keyboardCharacter = 'keyboard_character'.tr; // 键盘人设
+  static final String keyboardSave = 'keyboard_save'.tr; // 保存
+  static final String keyboardSaveSuccess = 'keyboard_save_success'.tr; // 保存成功
+  static final String keyboardSaveFailed = 'keyboard_save_failed'.tr; // 保存失败
+  static final String addCharacter = 'add_character'.tr; // 添加人设
+  static final String customCharacter = 'custom_character'.tr; // 定制人设
+  static final String characterCustomStepsDesc = 'character_custom_steps_desc'.tr; // 专属于你独一无二的人设
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -127,6 +137,16 @@ class StringMultiSource {
       'load_completed': '加载完成',
       'add_to_keyboard': '添加到键盘',
       'added_to_keyboard': '已添加到键盘',
+      'keyboard_custom': '定制键盘',
+      'general_keyboard': '通用键盘',
+      'intimacy': '亲密度',
+      'keyboard_character': '键盘人设',
+      'keyboard_save': '保存',
+      'keyboard_save_success': '保存成功',
+      'keyboard_save_failed': '保存失败',
+      'add_character': '添加人设',
+      'custom_character': '定制人设',
+      'character_custom_steps_desc': '专属于你独一无二的人设',
     },
   };
 }

+ 10 - 0
lib/router/app_pages.dart

@@ -2,8 +2,10 @@ import 'package:get/get.dart';
 import 'package:keyboard/module/about/about_controller.dart';
 import 'package:keyboard/module/browser/browser_controller.dart';
 import 'package:keyboard/module/character/content/character_group_content_controller.dart';
+import 'package:keyboard/module/character_custom/character_custom_controller.dart';
 import 'package:keyboard/module/feedback/feedback_controller.dart';
 import 'package:keyboard/module/keyboard/keyboard_controller.dart';
+import 'package:keyboard/module/keyboard_manage/keyboard_manage_controller.dart';
 
 import 'package:keyboard/module/login/login_controller.dart';
 import 'package:keyboard/module/mine/mine_controller.dart';
@@ -14,7 +16,9 @@ import '../di/get_it.dart';
 import '../module/about/about_page.dart';
 import '../module/browser/browser_page.dart';
 import '../module/character/character_controller.dart';
+import '../module/character_custom/character_custom_page.dart';
 import '../module/feedback/feedback_page.dart';
+import '../module/keyboard_manage/keyboard_manage_page.dart';
 import '../module/login/login_page.dart';
 import '../module/main/main_controller.dart';
 import '../module/main/main_page.dart';
@@ -31,6 +35,8 @@ abstract class RoutePath {
   static const feedback = '/feedback';
   static const about = '/about';
   static const browser = '/browser';
+  static const keyboardManage = '/keyboardManage';
+  static const characterCustom = '/characterCustom';
 }
 
 class AppBinding extends Bindings {
@@ -45,6 +51,8 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<AboutController>());
     lazyPut(() => getIt.get<BrowserController>());
     lazyPut(() => getIt.get<CharacterGroupContentController>());
+    lazyPut(() => getIt.get<KeyboardManageController>());
+    lazyPut(() => getIt.get<CharacterCustomController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -58,4 +66,6 @@ final generalPages = [
   GetPage(name: RoutePath.feedback, page: () => FeedbackPage()),
   GetPage(name: RoutePath.about, page: () => AboutPage()),
   GetPage(name: RoutePath.browser, page: () => BrowserPage()),
+  GetPage(name: RoutePath.keyboardManage, page: () => KeyboardManagePage()),
+  GetPage(name: RoutePath.characterCustom, page: () => CharacterCustomPage()),
 ];

+ 90 - 0
lib/widget/gradient_rect_slider_track_shape.dart

@@ -0,0 +1,90 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+/// 自定义带有渐变的 Slider 轨道
+class GradientRectSliderTrackShape extends SliderTrackShape {
+  const GradientRectSliderTrackShape();
+
+  @override
+  Rect getPreferredRect({
+    required RenderBox parentBox,
+    Offset offset = Offset.zero,
+    required SliderThemeData sliderTheme,
+    bool isEnabled = false,
+    bool isDiscrete = false,
+  }) {
+    final double trackHeight = sliderTheme.trackHeight ?? 4;
+    final double trackLeft = offset.dx;
+    final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
+    final double trackWidth = parentBox.size.width;
+    return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
+  }
+
+  @override
+  void paint(
+      PaintingContext context,
+      Offset offset, {
+        required RenderBox parentBox,
+        required SliderThemeData sliderTheme,
+        required Animation<double> enableAnimation,
+        required TextDirection textDirection,
+        required Offset thumbCenter,
+        bool isDiscrete = false,
+        bool isEnabled = false,
+        Offset? secondaryOffset,
+        double additionalActiveTrackHeight = 0,
+      }) {
+    // 轨道矩形区域
+    final Rect trackRect = getPreferredRect(
+      parentBox: parentBox,
+      offset: offset,
+      sliderTheme: sliderTheme,
+      isEnabled: isEnabled,
+      isDiscrete: isDiscrete,
+    );
+
+    // 激活部分的宽度
+    final activeRect = Rect.fromLTRB(
+      trackRect.left,
+      trackRect.top,
+      thumbCenter.dx,
+      trackRect.bottom,
+    );
+
+    // 非激活部分的宽度
+    final inactiveRect = Rect.fromLTRB(
+      thumbCenter.dx,
+      trackRect.top,
+      trackRect.right,
+      trackRect.bottom,
+    );
+
+    final Canvas canvas = context.canvas;
+
+    // 绘制激活部分(渐变)
+    final activePaint = Paint()..shader = const LinearGradient(
+      begin: Alignment(0.04, 0.21),
+      end: Alignment(0.98, 0.76),
+      colors: [Color(0xFFD5BAF8),Color(0xFFAA88FA) ],
+    ).createShader(activeRect);
+
+    // 绘制非激活部分
+    final inactivePaint = Paint()
+      ..color = sliderTheme.inactiveTrackColor ?? Colors.grey.shade200;
+
+    // 圆角半径
+    final radius = Radius.circular(trackRect.height / 2);
+
+    // 绘制轨道
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(inactiveRect, radius),
+      inactivePaint,
+    );
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(activeRect, radius),
+      activePaint,
+    );
+  }
+}
+
+

+ 33 - 0
lib/widget/tab_custom_gradient_indicator.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+class TabCustomGradientIndicator extends Decoration {
+  @override
+  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
+    return _TabCustomGradientIndicatorPainter();
+  }
+}
+
+class _TabCustomGradientIndicatorPainter extends BoxPainter {
+  @override
+  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
+    final double indicatorWidth = 16.0.w;
+    final double indicatorHeight = 4.0.h;
+    final double dx =
+        offset.dx + (configuration.size!.width - indicatorWidth) / 2;
+    final double dy =
+        offset.dy + configuration.size!.height - indicatorHeight - 2;
+    final Rect rect = Rect.fromLTWH(dx, dy, indicatorWidth, indicatorHeight);
+    final RRect rRect = RRect.fromRectAndRadius(rect, Radius.circular(4));
+
+    final Paint paint =
+        Paint()
+          ..shader = LinearGradient(
+            begin: Alignment(0.04, 0.21),
+            end: Alignment(0.98, 0.76),
+            colors: [Color(0xFF7D46FC), Color(0xFFBC87FF)],
+          ).createShader(rect);
+
+    canvas.drawRRect(rRect, paint);
+  }
+}

+ 73 - 0
plugins/reorderables/.gitignore

@@ -0,0 +1,73 @@
+# Miscellaneous
+*.class
+*.lock
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Visual Studio Code related
+.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.packages
+.pub-cache/
+.pub/
+build/
+.fvm/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/Flutter/flutter_assets/
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

+ 10 - 0
plugins/reorderables/.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 985ccb6d14c6ce5ce74823a4d366df2438eac44f
+  channel: beta
+
+project_type: package

+ 113 - 0
plugins/reorderables/CHANGELOG.md

@@ -0,0 +1,113 @@
+## [0.6.0] - 25 Jan 2023.
+* Bugfix: [#174](https://github.com/hanshengchiu/reorderables/pull/174). Fix crashes when using Flutter 3.7 (thanks [luccasclezar](https://github.com/luccasclezar))
+
+## [0.5.1] - 3 Nov 2022.
+* New Feature: [#164](https://github.com/hanshengchiu/reorderables/pull/164). add a draggedItemBuilder widget to support persistent state children (thanks [danReynolds](https://github.com/danReynolds))
+
+## [0.5.0] - 12 May 2022.
+* Bugfix: [#51](https://github.com/hanshengchiu/reorderables/issues/151). Added lint fixes due to Flutter 3.
+  (thanks [diegotori](https://github.com/diegotori)).
+* ***Breaking Change***: Library now requires Flutter version `3.0.0` or higher.
+
+## [0.4.4] - 18 April 2022.
+* Bugfix: [#146](https://github.com/hanshengchiu/reorderables/issues/146). ReorderableWrap: adding scroll controller causes exception
+  (thanks [diegotori](https://github.com/diegotori)).
+
+## [0.4.3] - 22 February 2022.
+* New Feature: [#143](https://github.com/hanshengchiu/reorderables/pull/143). Allow enableReorder on DraggableWrap
+  (thanks [89jd](https://github.com/89jd)).
+* Removed redundant import statements due to latest Dart analysis rules.  
+
+## [0.4.2] - 01 December 2021.
+* Bugfix: [#75](https://github.com/hanshengchiu/reorderables/issues/75). Add ReorderStartedCallback to ReorderableFlex, Change ReorderableFlex Draggable data to index
+  (thanks [pxsanghyo](https://github.com/pxsanghyo)).
+* New Feature: [#111](https://github.com/hanshengchiu/reorderables/pull/121). Fix: Made Non-ReorderableWrap item non-droppable and non-draggable
+  (thanks [avi-yadav](https://github.com/avi-yadav)).
+* New Feature: [#136](https://github.com/hanshengchiu/reorderables/pull/136). Ft: add scroll phyiscs to SingleChildScrollView in ReorderableWrap
+  (thanks [pcvdheuvel](https://github.com/pcvdheuvel)).
+* New Feature: [#138](https://github.com/hanshengchiu/reorderables/pull/138). Flutter 2.5 deprecation fixes
+  (thanks [diegotori](https://github.com/diegotori)).
+  
+## [0.4.1] - 11 April 2021.
+* Addresses Issue [#111](https://github.com/hanshengchiu/reorderables/issues/111). Resolves ReorderableSliverList ScrollController conflict (thanks [qAison](https://github.com/qAison)).
+
+## [0.4.0] - 24 March 2021.
+
+* Initial Null-Safety release.
+
+## [0.3.2] - 10 March 2020.
+* Fix health suggestions.
+
+## [0.3.1] - 10 March 2020.
+* Supports making individual child non-reorderable. See ReorderableColumn example 1.
+
+## [0.3.0] - 10 Jan 2020.
+* Fix: Bad type in onLeave
+
+## [0.2.12] - 22 June 2019.
+* Removed dependency of FlutterErrorDetails and other ErrorXXX classes.
+* Bugfix: needsLongPressDraggable had no default value.
+
+## [0.2.11+1] - 11 June 2019.
+* Flutter version dependency in pubspec
+
+## [0.2.11] - 11 June 2019.
+
+* ReorderableRow and ReorderableColumn:
+Set needsLongPressDraggable to false to use Draggable.
+Provide scrollController if use of external scroller controller is preferred.
+* Added onReorderStarted callback in ReorderableWrap
+* updated README
+
+## [0.2.10] - 10 June 2019.
+
+* Bugfix: DiagnosticsNode instead of String for newer version of Flutter
+
+## [0.2.9] - 16 May 2019.
+
+* Allows use of CupertinoApp
+
+## [0.2.8] - 16 May 2019.
+
+* Added onNoReorder callback
+
+## [0.2.7] - 11 May 2019.
+
+* Sliver's cross axis alignment defaults to start
+* Remove the use of global keys in reorderable sliver to allow nested sliver
+
+## [0.2.6] - 6 May 2019.
+
+* Bugfix: "width is null"
+
+## [0.2.5] - 3 May 2019.
+
+* Bugfix: ReorderableWrap supports nested wraps.
+* Improvement: children of ReorderableWrap don't have to have a key anymore.
+* Included nested ReorderableWrap example
+
+## [0.2.1] - 28 April 2019.
+
+* Bugfix: couldn't add/remove elements in ReorderableWrap.
+* Bugfix: added elements weren't draggable in ReorderableSliverList.
+* Merged pull request: flag to choose between long press draggable and the short one.
+
+## [0.2.0] - 5 March 2019.
+
+* Added ReorderableSliverList, ReorderableSliverChildBuilderDelegate, and ReorderableSliverChildListDelegate.
+* Bugfix: ReorderableFlex's animation.
+
+## [0.1.6] - 1 March 2019.
+
+* Updated API references and README.
+* Bugfix: made ReorderableTable's onReorder required.
+* Bugfix: corrected scrollDirection in ReorderableRow.
+
+## [0.1.5] - 26 February 2019.
+
+* Updated API references.
+
+## [0.1.4] - 25 February 2019.
+
+* Alignment bugfix.
+* Added column examples.

+ 21 - 0
plugins/reorderables/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Hansheng Chiu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 503 - 0
plugins/reorderables/README.md

@@ -0,0 +1,503 @@
+** Kindly submit PR if you encounter issues and please make sure you're using stable channel releases. <br>
+** Maintaining open source software ain't easy. If you've commercially used this software, please consider [support](https://www.buymeacoffee.com/q5gkeA4t2).
+
+# reorderables
+
+[![pub package](https://img.shields.io/pub/v/reorderables.svg)](https://pub.dartlang.org/packages/reorderables)
+[![Awesome Flutter](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter)
+[![Buy Me A Coffee](https://img.shields.io/badge/Donate-Buy%20Me%20A%20Coffee-yellow.svg)](https://www.buymeacoffee.com/q5gkeA4t2)
+
+
+Various reorderable, a.k.a. drag and drop, Flutter widgets, including reorderable table, row, column, wrap, and sliver list, that make their children draggable and 
+reorder them within the widget. Parent widget only need to provide an `onReorder` function that is invoked with the old and new indexes of child being reordered.
+
+
+## Usage
+To use this [package](https://pub.dartlang.org/packages/reorderables), add `reorderables` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+```
+dependencies:
+  reorderables:
+```
+And import the package in your code.
+``` dart
+import 'package:reorderables/reorderables.dart';
+```
+## Examples
+
+This package includes ReorderableSliverList, ReorderableTable, ReorderableWrap, ReorderableRow, and ReorderableColumn, which are reorderable versions of Flutter's SliverList, Table, Wrap, Row, and Column respectively.
+
+<p>
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_sliver_small.gif?raw=true" width="180" title="ReorderableSliverList">
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_table_small.gif?raw=true" width="180" title="ReorderableTable">
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_wrap_small.gif?raw=true" width="180" title="ReorderableWrap">
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/nested_reorderable_wrap_small.gif?raw=true" width="180" title="Nested ReorderableWrap">
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_column1_small.gif?raw=true" width="180" title="ReorderableColumn #1">
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_column2_small.gif?raw=true" width="180" title="ReorderableColumn #2">
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_row_small.gif?raw=true" width="180" title="ReorderableRow">
+</p>
+
+### ReorderableSliverList
+
+ReorderableSliverList behaves exactly like SliverList, but its children are draggable.
+
+To make a SliverList reorderable, replace it with ReorderableSliverList and replace SliverChildListDelegate/SliverChildBuilderDelegate with ReorderableSliverChildListDelegate/ReorderableSliverChildBuilderDelegate.
+Do make sure that there's a ScrollController attached to the ScrollView that contains ReorderableSliverList, otherwise an error will be thrown when dragging list items.
+
+``` dart
+class _SliverExampleState extends State<SliverExample> {
+  List<Widget> _rows;
+
+  @override
+  void initState() {
+    super.initState();
+    _rows = List<Widget>.generate(50,
+        (int index) => Text('This is sliver child $index', textScaleFactor: 2)
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    void _onReorder(int oldIndex, int newIndex) {
+      setState(() {
+        Widget row = _rows.removeAt(oldIndex);
+        _rows.insert(newIndex, row);
+      });
+    }
+    // Make sure there is a scroll controller attached to the scroll view that contains ReorderableSliverList.
+    // Otherwise an error will be thrown.
+    ScrollController _scrollController = PrimaryScrollController.of(context) ?? ScrollController();
+
+    return CustomScrollView(
+      // A ScrollController must be included in CustomScrollView, otherwise
+      // ReorderableSliverList wouldn't work
+      controller: _scrollController,
+      slivers: <Widget>[
+        SliverAppBar(
+          expandedHeight: 210.0,
+          flexibleSpace: FlexibleSpaceBar(
+            title: Text('ReorderableSliverList'),
+            background: Image.network(
+              'https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Yushan'
+                '_main_east_peak%2BHuang_Chung_Yu%E9%BB%83%E4%B8%AD%E4%BD%91%2B'
+                '9030.png/640px-Yushan_main_east_peak%2BHuang_Chung_Yu%E9%BB%83'
+                '%E4%B8%AD%E4%BD%91%2B9030.png'),
+          ),
+        ),
+        ReorderableSliverList(
+          delegate: ReorderableSliverChildListDelegate(_rows),
+          // or use ReorderableSliverChildBuilderDelegate if needed
+//          delegate: ReorderableSliverChildBuilderDelegate(
+//            (BuildContext context, int index) => _rows[index],
+//            childCount: _rows.length
+//          ),
+          onReorder: _onReorder,
+        )
+      ],
+    );
+  }
+}
+```
+
+#### ReorderableSliverList Demo
+
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_sliver_small.gif?raw=true" width="360" title="ReorderableSliverList">
+
+### ReorderableTable
+
+The difference between table and list is that cells in a table are horizontally aligned, whereas in a list, each item can have children but they are not aligned with children in another item.
+
+Making a row draggable requires cells to be contained in a single widget. This isn't achievable with Table or GridView widget since their children are laid out as cells of widget instead of rows of widget.
+
+``` dart
+class _TableExampleState extends State<TableExample> {
+  List<ReorderableTableRow> _itemRows;
+
+  @override
+  void initState() {
+    super.initState();
+    var data = [
+      ['Alex', 'D', 'B+', 'AA', ''],
+      ['Bob', 'AAAAA+', '', 'B', ''],
+      ['Cindy', '', 'To Be Confirmed', '', ''],
+      ['Duke', 'C-', '', 'Failed', ''],
+      ['Ellenina', 'C', 'B', 'A', 'A'],
+      ['Floral', '', 'BBB', 'A', 'A'],
+    ];
+
+    Widget _textWithPadding(String text) {
+      return Padding(
+        padding: EdgeInsets.symmetric(vertical: 4),
+        child: Text(text, textScaleFactor: 1.1),
+      );
+    }
+
+    _itemRows = data.map((row) {
+      return ReorderableTableRow(
+        //a key must be specified for each row
+        key: ObjectKey(row),
+        mainAxisSize: MainAxisSize.max,
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: <Widget>[
+          _textWithPadding('${row[0]}'),
+          _textWithPadding('${row[1]}'),
+          _textWithPadding('${row[2]}'),
+          _textWithPadding('${row[3]}'),
+//          Text('${row[4]}'),
+        ],
+      );
+    }).toList();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    var headerRow = ReorderableTableRow(
+      mainAxisSize: MainAxisSize.max,
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: [
+        Text('Name', textScaleFactor: 1.5),
+        Text('Math', textScaleFactor: 1.5),
+        Text('Science', textScaleFactor: 1.5),
+        Text('Physics', textScaleFactor: 1.5),
+        Text('Sports', textScaleFactor: 1.5)
+      ]
+    );
+
+    void _onReorder(int oldIndex, int newIndex) {
+      setState(() {
+        ReorderableTableRow row = _itemRows.removeAt(oldIndex);
+        _itemRows.insert(newIndex, row);
+      });
+    }
+
+    return ReorderableTable(
+      header: headerRow,
+      children: _itemRows,
+      onReorder: _onReorder,
+    );
+  }
+}
+```
+
+In a table, cells in each row are aligned on column basis with cells in other rows, 
+whereas cells in a row of a list view don't align with  other rows.
+
+#### ReorderableTable Demo
+
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_table_small.gif?raw=true" width="360" title="ReorderableTable">
+
+### ReorderableWrap
+
+This widget can also limit the minimum and maximum amount of children in each run, on top of the size-based policy in Wrap's algorithm. See API references for more details.
+*Since v0.2.5, children of ReorderableWrap don't need to have a key explicitly specified.
+
+``` dart
+class _WrapExampleState extends State<WrapExample> {
+  final double _iconSize = 90;
+  List<Widget> _tiles;
+
+  @override
+  void initState() {
+    super.initState();
+    _tiles = <Widget>[
+      Icon(Icons.filter_1, size: _iconSize),
+      Icon(Icons.filter_2, size: _iconSize),
+      Icon(Icons.filter_3, size: _iconSize),
+      Icon(Icons.filter_4, size: _iconSize),
+      Icon(Icons.filter_5, size: _iconSize),
+      Icon(Icons.filter_6, size: _iconSize),
+      Icon(Icons.filter_7, size: _iconSize),
+      Icon(Icons.filter_8, size: _iconSize),
+      Icon(Icons.filter_9, size: _iconSize),
+    ];
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    void _onReorder(int oldIndex, int newIndex) {
+      setState(() {
+        Widget row = _tiles.removeAt(oldIndex);
+        _tiles.insert(newIndex, row);
+      });
+    }
+
+    var wrap = ReorderableWrap(
+      spacing: 8.0,
+      runSpacing: 4.0,
+      padding: const EdgeInsets.all(8),
+      children: _tiles,
+      onReorder: _onReorder,
+       onNoReorder: (int index) {
+        //this callback is optional
+        debugPrint('${DateTime.now().toString().substring(5, 22)} reorder cancelled. index:$index');
+      },
+      onReorderStarted: (int index) {
+        //this callback is optional
+        debugPrint('${DateTime.now().toString().substring(5, 22)} reorder started: index:$index');
+      }
+    );
+
+    var column = Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: <Widget>[
+        wrap,
+        ButtonBar(
+          alignment: MainAxisAlignment.start,
+          children: <Widget>[
+            IconButton(
+              iconSize: 50,
+              icon: Icon(Icons.add_circle),
+              color: Colors.deepOrange,
+              padding: const EdgeInsets.all(0.0),
+              onPressed: () {
+                var newTile = Icon(Icons.filter_9_plus, size: _iconSize);
+                setState(() {
+                  _tiles.add(newTile);
+                });
+              },
+            ),
+            IconButton(
+              iconSize: 50,
+              icon: Icon(Icons.remove_circle),
+              color: Colors.teal,
+              padding: const EdgeInsets.all(0.0),
+              onPressed: () {
+                setState(() {
+                  _tiles.removeAt(0);
+                });
+              },
+            ),
+          ],
+        ),
+      ],
+    );
+
+    return SingleChildScrollView(
+      child: column,
+    );
+
+  }
+}
+```
+
+#### ReorderableWrap Demo
+
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_wrap_small.gif?raw=true" width="360" title="ReorderableWrap">
+
+### Nested ReorderableWrap
+
+It is also possible to nest multiple levels of ReorderableWrap. See `example/lib/nested_wrap_example.dart` for complete example code.
+
+``` dart
+class _NestedWrapExampleState extends State<NestedWrapExample> {
+//  List<Widget> _tiles;
+  Color _color;
+  Color _colorBrighter;
+
+  @override
+  void initState() {
+    super.initState();
+    _color = widget.color ?? Colors.primaries[widget.depth % Colors.primaries.length];
+    _colorBrighter = Color.lerp(_color, Colors.white, 0.6);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    void _onReorder(int oldIndex, int newIndex) {
+      setState(() {
+        widget._tiles.insert(newIndex, widget._tiles.removeAt(oldIndex));
+      });
+    }
+
+    var wrap = ReorderableWrap(
+      spacing: 8.0,
+      runSpacing: 4.0,
+      padding: const EdgeInsets.all(8),
+      children: widget._tiles,
+      onReorder: _onReorder
+    );
+
+    var buttonBar = Container(
+      color: _colorBrighter,
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        mainAxisAlignment: MainAxisAlignment.start,
+        children: <Widget>[
+          IconButton(
+            iconSize: 42,
+            icon: Icon(Icons.add_circle),
+            color: Colors.deepOrange,
+            padding: const EdgeInsets.all(0.0),
+            onPressed: () {
+              setState(() {
+                widget._tiles.add(
+                  Card(
+                    child: Container(
+                      child: Text('${widget.valuePrefix}${widget._tiles.length}', textScaleFactor: 3 / math.sqrt(widget.depth + 1)),
+                      padding: EdgeInsets.all((24.0 / math.sqrt(widget.depth + 1)).roundToDouble()),
+                    ),
+                    color: _colorBrighter,
+                    elevation: 3,
+                  )
+                );
+              });
+            },
+          ),
+          IconButton(
+            iconSize: 42,
+            icon: Icon(Icons.remove_circle),
+            color: Colors.teal,
+            padding: const EdgeInsets.all(0.0),
+            onPressed: () {
+              setState(() {
+                widget._tiles.removeAt(0);
+              });
+            },
+          ),
+          IconButton(
+            iconSize: 42,
+            icon: Icon(Icons.add_to_photos),
+            color: Colors.pink,
+            padding: const EdgeInsets.all(0.0),
+            onPressed: () {
+              setState(() {
+                widget._tiles.add(NestedWrapExample(depth: widget.depth + 1, valuePrefix: '${widget.valuePrefix}${widget._tiles.length}.',));
+              });
+            },
+          ),
+          Text('Level ${widget.depth} / ${widget.valuePrefix}'),
+        ],
+      )
+    );
+
+    var column = Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        buttonBar,
+        wrap,
+      ]
+    );
+
+    return SingleChildScrollView(
+      child: Container(child: column, color: _color,),
+    );
+  }
+}
+```
+
+#### Nested ReorderableWrap Demo
+
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/nested_reorderable_wrap_small.gif?raw=true" width="360" title="Nested ReorderableWrap">
+
+### ReorderableColumn example #1
+
+``` dart
+class _ColumnExample1State extends State<ColumnExample1> {
+  List<Widget> _rows;
+
+  @override
+  void initState() {
+    super.initState();
+    _rows = List<Widget>.generate(50,
+        (int index) => Text('This is row $index', key: ValueKey(index), textScaleFactor: 1.5)
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    void _onReorder(int oldIndex, int newIndex) {
+      setState(() {
+        Widget row = _rows.removeAt(oldIndex);
+        _rows.insert(newIndex, row);
+      });
+    }
+
+    return ReorderableColumn(
+      header: Text('THIS IS THE HEADER ROW'),
+      footer: Text('THIS IS THE FOOTER ROW'),
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: _rows,
+      onReorder: _onReorder,
+    );
+  }
+}
+```
+
+#### ReorderableColumn example #1 Demo
+
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_column1_small.gif?raw=true" width="360" title="ReorderableColumn #1">
+
+### ReorderableColumn example #2
+
+``` dart
+class _ColumnExample2State extends State<ColumnExample2> {
+  List<Widget> _rows;
+
+  @override
+  void initState() {
+    super.initState();
+    _rows = List<Widget>.generate(10,
+        (int index) => Text('This is row $index', key: ValueKey(index), textScaleFactor: 1.5)
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    void _onReorder(int oldIndex, int newIndex) {
+      setState(() {
+        Widget row = _rows.removeAt(oldIndex);
+        _rows.insert(newIndex, row);
+      });
+    }
+
+    Widget reorderableColumn = IntrinsicWidth(
+      child: ReorderableColumn(
+        header: Text('List-like view but supports IntrinsicWidth'),
+//        crossAxisAlignment: CrossAxisAlignment.start,
+        children: _rows,
+        onReorder: _onReorder,
+      )
+    );
+
+    return Transform(
+      transform: Matrix4.rotationZ(0),
+      alignment: FractionalOffset.topLeft,
+      child: Material(
+        child: Card(child: reorderableColumn),
+        elevation: 6.0,
+        color: Colors.transparent,
+        borderRadius: BorderRadius.zero,
+      ),
+    );
+  }
+}
+```
+
+#### ReorderableColumn example #2 Demo
+
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_column2_small.gif?raw=true" width="360" title="ReorderableColumn #2">
+
+### ReorderableRow
+
+See `exmaple/lib/row_example.dart`
+
+#### ReorderableRow Demo
+
+<img src="https://github.com/hanshengchiu/reorderables/blob/master/example/gifs/reorderable_row_small.gif?raw=true" width="360" title="ReorderableRow">
+
+## Issues
+
+I've switched to Flutter channel stable from beta in order avoid compatibility issues. Supporting master or dev channels is not intended as they change frequently. 
+Kindly make sure that you are using stable channel when submitting issues.
+
+## Support
+
+If you need `commercial support`, please reach out to me by sending me message on LinkedIn [![Hansheng](https://img.shields.io/badge/Consult%20Me-E68700.svg)](https://www.linkedin.com/in/hschiu/) 
+
+Otherwise, you can also support me by buying me a coffee or donate me via PayPal.
+Your support is very much appreciated. :)
+
+[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-yellow.svg)](https://www.buymeacoffee.com/q5gkeA4t2) 
+ or 
+[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=2L56VGH228QJE)
+ or
+[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/hanshengchiu) 

+ 7 - 0
plugins/reorderables/lib/reorderables.dart

@@ -0,0 +1,7 @@
+library reorderables;
+
+export 'src/widgets/reorderable_flex.dart';
+export 'src/widgets/reorderable_sliver.dart';
+export 'src/widgets/reorderable_table.dart';
+export 'src/widgets/reorderable_widget.dart';
+export 'src/widgets/reorderable_wrap.dart';

+ 902 - 0
plugins/reorderables/lib/src/rendering/tabluar_flex.dart

@@ -0,0 +1,902 @@
+import 'dart:collection';
+import 'dart:math' as math;
+
+import 'package:flutter/rendering.dart';
+
+bool? _startIsTopLeft(Axis direction, TextDirection? textDirection,
+    VerticalDirection verticalDirection) {
+  // If the relevant value of textDirection or verticalDirection is null, this returns null too.
+  return direction == Axis.horizontal
+      ? (textDirection == null ? null : textDirection == TextDirection.ltr)
+      : verticalDirection == VerticalDirection.down;
+}
+
+typedef _ChildSizingFunction = double Function(RenderBox child, double extent);
+
+class RenderTabluarFlex extends RenderFlex {
+  /// Creates a flex render object.
+  ///
+  /// By default, the flex layout is horizontal and children are aligned to the
+  /// start of the main axis and the center of the cross axis.
+  RenderTabluarFlex({
+    List<RenderBox>? children,
+    Axis direction = Axis.horizontal,
+    MainAxisSize mainAxisSize = MainAxisSize.max,
+    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
+    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
+    TextDirection? textDirection,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    TextBaseline? textBaseline,
+    Decoration? decoration,
+    ImageConfiguration configuration = ImageConfiguration.empty,
+  })  : _decoration = decoration,
+        _configuration = configuration,
+        super(
+          children: children,
+          direction: direction,
+          mainAxisSize: mainAxisSize,
+          mainAxisAlignment: mainAxisAlignment,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+        );
+
+  BoxPainter? _painter;
+
+  /// What decoration to paint.
+  ///
+  /// Commonly a [BoxDecoration].
+  Decoration? get decoration => _decoration;
+  Decoration? _decoration;
+
+  set decoration(Decoration? value) {
+//    assert(value != null);
+    if (value == _decoration) return;
+    _painter?.dispose();
+    _painter = null;
+    _decoration = value;
+    markNeedsPaint();
+  }
+
+  /// The settings to pass to the decoration when painting, so that it can
+  /// resolve images appropriately. See [ImageProvider.resolve] and
+  /// [BoxPainter.paint].
+  ///
+  /// The [ImageConfiguration.textDirection] field is also used by
+  /// direction-sensitive [Decoration]s for painting and hit-testing.
+  ImageConfiguration get configuration => _configuration;
+  ImageConfiguration _configuration;
+
+  set configuration(ImageConfiguration value) {
+    if (value == _configuration) return;
+    _configuration = value;
+    markNeedsPaint();
+  }
+
+  @override
+  void detach() {
+    _painter?.dispose();
+    _painter = null;
+    super.detach();
+    // Since we're disposing of our painter, we won't receive change
+    // notifications. We mark ourselves as needing paint so that we will
+    // resubscribe to change notifications. If we didn't do this, then, for
+    // example, animated GIFs would stop animating when a DecoratedBox gets
+    // moved around the tree due to GlobalKey reparenting.
+    markNeedsPaint();
+  }
+
+  bool get _debugHasNecessaryDirections {
+    if (firstChild != null && lastChild != firstChild) {
+      // i.e. there's more than one child
+      assert(
+          direction == Axis.vertical || textDirection != null,
+          'Horizontal $runtimeType with multiple children has a '
+          'null textDirection, so the layout order is undefined.');
+    }
+    if (mainAxisAlignment == MainAxisAlignment.start ||
+        mainAxisAlignment == MainAxisAlignment.end) {
+      assert(
+          direction == Axis.vertical || textDirection != null,
+          'Horizontal $runtimeType with $mainAxisAlignment has a null '
+          'textDirection, so the alignment cannot be resolved.');
+    }
+    if (crossAxisAlignment == CrossAxisAlignment.start ||
+        crossAxisAlignment == CrossAxisAlignment.end) {
+      assert(
+          direction == Axis.horizontal || textDirection != null,
+          'Vertical $runtimeType with $crossAxisAlignment has a null '
+          'textDirection, so the alignment cannot be resolved.');
+    }
+    return true;
+  }
+
+  // Set during layout if overflow occurred on the main axis.
+  double _overflow = 0;
+
+  ///
+  /// Determines whether the current overflow value is greater than zero.
+  ///
+  bool get _hasOverflow => _overflow > 0.0;
+
+  final ListQueue<LayoutCallback<BoxConstraints>> layoutCallbackQueue =
+      ListQueue<LayoutCallback<BoxConstraints>>();
+  final ListQueue<Map<int, double>> minMainSizesQueue =
+      ListQueue<Map<int, double>>();
+  final Map<int, double> _maxGrandchildrenCrossSize = {};
+
+  Map<int, double> get maxGrandchildrenCrossSize =>
+      _maxGrandchildrenCrossSize; //  @override
+//  void setupParentData(RenderBox child) {
+//    if (child.parentData is! FlexParentData)
+//      child.parentData = FlexParentData();
+//  }
+
+  double _getIntrinsicSize(
+      {required Axis sizingDirection,
+      required double
+          extent, // the extent in the direction that isn't the sizing direction
+      required _ChildSizingFunction
+          childSize // a method to find the size in the sizing direction
+      }) {
+    if (direction == sizingDirection) {
+      // INTRINSIC MAIN SIZE
+      // Intrinsic main size is the smallest size the flex container can take
+      // while maintaining the min/max-content contributions of its flex items.
+      double totalFlex = 0.0;
+      double inflexibleSpace = 0.0;
+      double maxFlexFractionSoFar = 0.0;
+      RenderBox? child = firstChild;
+      while (child != null) {
+        final int flex = _getFlex(child);
+        totalFlex += flex;
+        if (flex > 0) {
+          final double flexFraction =
+              childSize(child, extent) / _getFlex(child);
+          maxFlexFractionSoFar = math.max(maxFlexFractionSoFar, flexFraction);
+        } else {
+          inflexibleSpace += childSize(child, extent);
+        }
+        final FlexParentData childParentData =
+            child.parentData! as FlexParentData;
+        child = childParentData.nextSibling;
+      }
+      return maxFlexFractionSoFar * totalFlex + inflexibleSpace;
+    } else {
+      // INTRINSIC CROSS SIZE
+      // Intrinsic cross size is the max of the intrinsic cross sizes of the
+      // children, after the flexible children are fit into the available space,
+      // with the children sized using their max intrinsic dimensions.
+      // TODO(ianh): Support baseline alignment.
+
+      // Get inflexible space using the max intrinsic dimensions of fixed children in the main direction.
+      final double availableMainSpace = extent;
+      int totalFlex = 0;
+      double inflexibleSpace = 0.0;
+      double maxCrossSize = 0.0;
+      RenderBox? child = firstChild;
+      Map<int, double> maxGrandchildCrossSize = {};
+      while (child != null) {
+        final int flex = _getFlex(child);
+        totalFlex += flex;
+        double mainSize;
+        double crossSize;
+        if (flex == 0) {
+          switch (direction) {
+            case Axis.horizontal:
+              mainSize = child.getMaxIntrinsicWidth(double.infinity);
+              crossSize = childSize(child, mainSize);
+              break;
+            case Axis.vertical:
+              mainSize = child.getMaxIntrinsicHeight(double.infinity);
+              crossSize = childSize(child, mainSize);
+              break;
+          }
+
+          RenderTabluarFlex? tabluarFlexChild =
+              _findTabluarFlexDescendant(child);
+          if (tabluarFlexChild is RenderTabluarFlex) {
+//            RenderTabluarFlex _child = child as RenderTabluarFlex;
+            List<RenderBox> grandchildren =
+                tabluarFlexChild.getChildrenAsList();
+            for (int i = 0; i < grandchildren.length; i++) {
+              double grandchildCrossSize =
+                  childSize(grandchildren[i], mainSize);
+              maxGrandchildCrossSize[i] =
+                  math.max(maxGrandchildCrossSize[i] ?? 0, grandchildCrossSize);
+            }
+//          double crossSize1 = childSize(child, mainSize);
+          }
+
+          inflexibleSpace += mainSize;
+          maxCrossSize = math.max(maxCrossSize, crossSize);
+        }
+        final FlexParentData childParentData =
+            child.parentData! as FlexParentData;
+        child = childParentData.nextSibling;
+      }
+
+      // Determine the spacePerFlex by allocating the remaining available space.
+      // When you're overconstrained spacePerFlex can be negative.
+      final double spacePerFlex =
+          math.max(0.0, (availableMainSpace - inflexibleSpace) / totalFlex);
+
+      // Size remaining (flexible) items, find the maximum cross size.
+      child = firstChild;
+      while (child != null) {
+        final int flex = _getFlex(child);
+        if (flex > 0) {
+          double mainSize = spacePerFlex * flex;
+          RenderTabluarFlex? tabluarFlexChild =
+              _findTabluarFlexDescendant(child);
+          if (tabluarFlexChild is RenderTabluarFlex) {
+            List<RenderBox> grandchildren =
+                tabluarFlexChild.getChildrenAsList();
+            for (int i = 0; i < grandchildren.length; i++) {
+              double grandchildCrossSize =
+                  childSize(grandchildren[i], mainSize);
+              maxGrandchildCrossSize[i] =
+                  math.max(maxGrandchildCrossSize[i] ?? 0, grandchildCrossSize);
+            }
+          }
+//          maxCrossSize = math.max(maxCrossSize, childSize(child, spacePerFlex * flex));
+          maxCrossSize = math.max(maxCrossSize, childSize(child, mainSize));
+        }
+        final FlexParentData childParentData =
+            child.parentData! as FlexParentData;
+        child = childParentData.nextSibling;
+      }
+
+      if (maxGrandchildCrossSize.isNotEmpty) {
+        maxCrossSize = math.max(
+            maxCrossSize,
+            maxGrandchildCrossSize.values
+                .reduce((value, element) => value + element));
+      }
+
+      return maxCrossSize;
+    }
+  }
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    return _getIntrinsicSize(
+        sizingDirection: Axis.horizontal,
+        extent: height,
+        childSize: (RenderBox child, double extent) =>
+            child.getMinIntrinsicWidth(extent));
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    return _getIntrinsicSize(
+        sizingDirection: Axis.horizontal,
+        extent: height,
+        childSize: (RenderBox child, double extent) =>
+            child.getMaxIntrinsicWidth(extent));
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    return _getIntrinsicSize(
+        sizingDirection: Axis.vertical,
+        extent: width,
+        childSize: (RenderBox child, double extent) =>
+            child.getMinIntrinsicHeight(extent));
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    return _getIntrinsicSize(
+        sizingDirection: Axis.vertical,
+        extent: width,
+        childSize: (RenderBox child, double extent) =>
+            child.getMaxIntrinsicHeight(extent));
+  }
+
+  int _getFlex(RenderBox child) {
+    final FlexParentData? childParentData = child.parentData as FlexParentData;
+    return childParentData?.flex ?? 0;
+  }
+
+  FlexFit _getFit(RenderBox? child) {
+    final FlexParentData? childParentData = child?.parentData as FlexParentData;
+    return childParentData?.fit ?? FlexFit.tight;
+  }
+
+  double _getCrossSize(RenderBox child) =>
+      direction == Axis.horizontal ? child.size.height : child.size.width;
+
+  double _getMainSize(RenderBox child) =>
+      direction == Axis.horizontal ? child.size.width : child.size.height;
+
+  RenderTabluarFlex? _findTabluarFlexDescendant(RenderBox child) {
+    RenderObject? curDescendant = child;
+    ListQueue<RenderObject> childrenQueue = ListQueue<RenderObject>();
+    while (curDescendant != null && curDescendant is! RenderTabluarFlex) {
+//      RenderObject firstChildRenderer;
+      curDescendant!.visitChildren((RenderObject renderObject) {
+//        firstChildRenderer ??= renderObject;
+        if (curDescendant is! RenderTabluarFlex) {
+          if (renderObject is RenderTabluarFlex) {
+            curDescendant = renderObject;
+          } else {
+            childrenQueue.addLast(renderObject);
+          }
+        }
+      });
+      if (curDescendant is! RenderTabluarFlex) {
+        curDescendant = childrenQueue.isNotEmpty
+            ? childrenQueue.removeFirst()
+            : null; //firstChildRenderer;
+      }
+    }
+
+    return curDescendant == null ? null : curDescendant as RenderTabluarFlex;
+  }
+
+  @override
+  void performLayout() {
+    assert(_debugHasNecessaryDirections);
+    // Determine used flex factor, size inflexible items, calculate free space.
+    int totalFlex = 0;
+    int totalChildren = 0;
+    final double maxMainSize = direction == Axis.horizontal
+        ? constraints.maxWidth
+        : constraints.maxHeight;
+    final bool canFlex = maxMainSize < double.infinity;
+//    debugPrint('${DateTime.now().toString().substring(5, 22)} tabluar_flex.dart(369) $this.performLayout');
+    Map<int, double> maxGrandchildrenCrossSize = {};
+
+    Map<RenderBox, RenderTabluarFlex> tabluarFlexDescendants = {};
+
+    void _layoutChild(RenderBox child, BoxConstraints constraints) {
+      RenderTabluarFlex? tabluarFlexChild = _findTabluarFlexDescendant(child);
+//      debugPrint('this:$this _layoutChild: child:$child tabluarFlexChild:$tabluarFlexChild');
+      if (tabluarFlexChild != null) {
+        //when this is laying out its child (and descendants), this function will be called. So that we can get the grandchild's size
+        bool callbackCalled = false;
+        void _childLayoutCallback(BoxConstraints constraints) {
+          List<RenderBox> grandchildren = tabluarFlexChild.getChildrenAsList();
+//          debugPrint('${DateTime.now().toString().substring(5, 22)} tabluar_flex.dart(381) $this._childLayoutCallback: grandchildren.length:${grandchildren.length}');
+          for (int i = 0; i < grandchildren.length; i++) {
+            double grandchildCrossSize = _getCrossSize(grandchildren[i]);
+            maxGrandchildrenCrossSize[i] = math.max(
+                maxGrandchildrenCrossSize[i] ?? 0, grandchildCrossSize);
+          }
+
+          callbackCalled = true;
+        }
+
+        tabluarFlexDescendants[child] =
+            tabluarFlexChild; //save this for later use
+
+        tabluarFlexChild.layoutCallbackQueue.addLast(_childLayoutCallback);
+        child.layout(constraints, parentUsesSize: true);
+        if (!callbackCalled) {
+          tabluarFlexChild.layout(constraints,
+              parentUsesSize:
+                  true); //make sure _childLayoutCallback will be called
+        }
+        tabluarFlexChild.layoutCallbackQueue.removeLast();
+//        debugPrint('this:$this tabluarFlexChild.constraints:${tabluarFlexChild.constraints}');
+      } else {
+        child.layout(constraints, parentUsesSize: true);
+      }
+    }
+
+    Map<int, double> minChildrenMainSize =
+        minMainSizesQueue.isNotEmpty ? minMainSizesQueue.last : {};
+
+    BoxConstraints _innerConstraints(int childIndex) {
+      BoxConstraints innerConstraints;
+      if (crossAxisAlignment == CrossAxisAlignment.stretch) {
+        switch (direction) {
+          case Axis.horizontal:
+            innerConstraints = BoxConstraints(
+                minHeight: constraints.maxHeight,
+                maxHeight: constraints.maxHeight);
+            break;
+          case Axis.vertical:
+            innerConstraints = BoxConstraints(
+                minWidth: constraints.maxWidth, maxWidth: constraints.maxWidth);
+            break;
+        }
+      } else {
+        switch (direction) {
+          case Axis.horizontal:
+            innerConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
+            break;
+          case Axis.vertical:
+            innerConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
+            break;
+        }
+      }
+
+      if (childIndex < minChildrenMainSize.length) {
+        switch (direction) {
+          case Axis.horizontal:
+            innerConstraints = innerConstraints.copyWith(
+                minWidth: math.max(innerConstraints.minWidth,
+                    minChildrenMainSize[childIndex]!));
+            break;
+          case Axis.vertical:
+            innerConstraints = innerConstraints.copyWith(
+                minHeight: math.max(innerConstraints.minHeight,
+                    minChildrenMainSize[childIndex]!));
+            break;
+        }
+      }
+
+      return innerConstraints;
+    }
+
+    Map<RenderBox, BoxConstraints> childrenConstraints = {};
+
+    double crossSize = 0.0;
+    double allocatedSize =
+        0.0; // Sum of the sizes of the non-flexible children.
+    RenderBox? child = firstChild;
+    RenderBox? lastFlexChild;
+    int childIndex = 0;
+    while (child != null) {
+      final FlexParentData childParentData =
+          child.parentData! as FlexParentData;
+      totalChildren++;
+      final int flex = _getFlex(child);
+      if (flex > 0) {
+        assert(() {
+          final String identity =
+              direction == Axis.horizontal ? 'row' : 'column';
+          final String axis =
+              direction == Axis.horizontal ? 'horizontal' : 'vertical';
+          final String dimension =
+              direction == Axis.horizontal ? 'width' : 'height';
+          String error, message;
+          String addendum = '';
+          if (!canFlex &&
+              (mainAxisSize == MainAxisSize.max ||
+                  _getFit(child) == FlexFit.tight)) {
+            error =
+                'RenderFlex children have non-zero flex but incoming $dimension constraints are unbounded.';
+            message =
+                'When a $identity is in a parent that does not provide a finite $dimension constraint, for example '
+                'if it is in a $axis scrollable, it will try to shrink-wrap its children along the $axis '
+                'axis. Setting a flex on a child (e.g. using Expanded) indicates that the child is to '
+                'expand to fill the remaining space in the $axis direction.';
+            final StringBuffer information = StringBuffer();
+            RenderBox? node = this;
+            switch (direction) {
+              case Axis.horizontal:
+                while (!node!.constraints.hasBoundedWidth &&
+                    node.parent is RenderBox) node = node.parent as RenderBox;
+                if (!node.constraints.hasBoundedWidth) node = null;
+                break;
+              case Axis.vertical:
+                while (!node!.constraints.hasBoundedHeight &&
+                    node.parent is RenderBox) node = node.parent as RenderBox;
+                if (!node.constraints.hasBoundedHeight) node = null;
+                break;
+            }
+            if (node != null) {
+              information.writeln(
+                  'The nearest ancestor providing an unbounded width constraint is:');
+              information.write('  ');
+              information.writeln(node.toStringShallow(joiner: '\n  '));
+            }
+            information.writeln('See also: https://flutter.io/layout/');
+            addendum = information.toString();
+          } else {
+            return true;
+          }
+          throw FlutterError('$error\n'
+              '$message\n'
+              'These two directives are mutually exclusive. If a parent is to shrink-wrap its child, the child '
+              'cannot simultaneously expand to fit its parent.\n'
+              'Consider setting mainAxisSize to MainAxisSize.min and using FlexFit.loose fits for the flexible '
+              'children (using Flexible rather than Expanded). This will allow the flexible children '
+              'to size themselves to less than the infinite remaining space they would otherwise be '
+              'forced to take, and then will cause the RenderFlex to shrink-wrap the children '
+              'rather than expanding to fit the maximum constraints provided by the parent.\n'
+              'The affected RenderFlex is:\n'
+              '  $this\n'
+              'The creator information is set to:\n'
+              '  $debugCreator\n'
+              '$addendum'
+              'If this message did not help you determine the problem, consider using debugDumpRenderTree():\n'
+              '  https://flutter.io/debugging/#rendering-layer\n'
+              '  http://docs.flutter.io/flutter/rendering/debugDumpRenderTree.html\n'
+              'If none of the above helps enough to fix this problem, please don\'t hesitate to file a bug:\n'
+              '  https://github.com/flutter/flutter/issues/new?template=BUG.md');
+        }());
+        totalFlex += childParentData.flex!;
+        lastFlexChild = child;
+      } else {
+        BoxConstraints innerConstraints = _innerConstraints(childIndex);
+
+//        debugPrint('${DateTime.now().toString().substring(5, 22)} tabluar_flex.dart(515) $this.performLayout: innerConstraints:$innerConstraints');
+//        child.layout(innerConstraints, parentUsesSize: true);
+        _layoutChild(child, innerConstraints);
+        childrenConstraints[child] = innerConstraints; //save this for later use
+
+        allocatedSize += _getMainSize(child);
+        crossSize = math.max(crossSize, _getCrossSize(child));
+      }
+      assert(child.parentData == childParentData);
+      child = childParentData.nextSibling;
+      childIndex++;
+    }
+
+    for (; childIndex < minChildrenMainSize.length; childIndex++) {
+      totalChildren++;
+
+      BoxConstraints innerConstraints = _innerConstraints(childIndex);
+      switch (direction) {
+        case Axis.horizontal:
+          allocatedSize += innerConstraints.minWidth;
+          break;
+        case Axis.vertical:
+          allocatedSize += innerConstraints.minHeight;
+          break;
+      }
+    }
+
+    // Distribute free space to flexible children, and determine baseline.
+    final double freeSpace =
+        math.max(0.0, (canFlex ? maxMainSize : 0.0) - allocatedSize);
+    double allocatedFlexSpace = 0.0;
+    double maxBaselineDistance = 0.0;
+    if (totalFlex > 0 || crossAxisAlignment == CrossAxisAlignment.baseline) {
+      final double spacePerFlex =
+          canFlex && totalFlex > 0 ? (freeSpace / totalFlex) : double.nan;
+      child = firstChild;
+      childIndex = 0;
+      while (child != null) {
+        final int flex = _getFlex(child);
+        if (flex > 0) {
+          final double maxChildExtent = canFlex
+              ? (child == lastFlexChild
+                  ? (freeSpace - allocatedFlexSpace)
+                  : spacePerFlex * flex)
+              : double.infinity;
+          double minChildExtent;
+          switch (_getFit(child)) {
+            case FlexFit.tight:
+              assert(maxChildExtent < double.infinity);
+              minChildExtent = maxChildExtent;
+              break;
+            case FlexFit.loose:
+              minChildExtent = 0.0;
+              break;
+          }
+          BoxConstraints innerConstraints;
+          if (crossAxisAlignment == CrossAxisAlignment.stretch) {
+            switch (direction) {
+              case Axis.horizontal:
+                innerConstraints = BoxConstraints(
+                    minWidth: minChildExtent,
+                    maxWidth: maxChildExtent,
+                    minHeight: constraints.maxHeight,
+                    maxHeight: constraints.maxHeight);
+                break;
+              case Axis.vertical:
+                innerConstraints = BoxConstraints(
+                    minWidth: constraints.maxWidth,
+                    maxWidth: constraints.maxWidth,
+                    minHeight: minChildExtent,
+                    maxHeight: maxChildExtent);
+                break;
+            }
+          } else {
+            switch (direction) {
+              case Axis.horizontal:
+                innerConstraints = BoxConstraints(
+                    minWidth: minChildExtent,
+                    maxWidth: maxChildExtent,
+                    maxHeight: constraints.maxHeight);
+                break;
+              case Axis.vertical:
+                innerConstraints = BoxConstraints(
+                    maxWidth: constraints.maxWidth,
+                    minHeight: minChildExtent,
+                    maxHeight: maxChildExtent);
+                break;
+            }
+          }
+
+          if (childIndex < minChildrenMainSize.length) {
+            switch (direction) {
+              case Axis.horizontal:
+                innerConstraints = innerConstraints.copyWith(
+                    minWidth: math.max(innerConstraints.minWidth,
+                        minChildrenMainSize[childIndex]!));
+                break;
+              case Axis.vertical:
+                innerConstraints = innerConstraints.copyWith(
+                    minHeight: math.max(innerConstraints.minHeight,
+                        minChildrenMainSize[childIndex]!));
+                break;
+            }
+          }
+
+//          child.layout(innerConstraints, parentUsesSize: true);
+          _layoutChild(child, innerConstraints);
+          childrenConstraints[child] = innerConstraints;
+
+          final double childSize = _getMainSize(child);
+          assert(childSize <= maxChildExtent);
+          allocatedSize += childSize;
+          allocatedFlexSpace += maxChildExtent;
+          crossSize = math.max(crossSize, _getCrossSize(child));
+        }
+        if (crossAxisAlignment == CrossAxisAlignment.baseline) {
+          assert(() {
+            if (textBaseline == null)
+              throw FlutterError(
+                  'To use FlexAlignItems.baseline, you must also specify which baseline to use using the "baseline" argument.');
+            return true;
+          }());
+          final double? distance =
+              child.getDistanceToBaseline(textBaseline!, onlyReal: true);
+          if (distance != null)
+            maxBaselineDistance = math.max(maxBaselineDistance, distance);
+        }
+        final FlexParentData childParentData =
+            child.parentData! as FlexParentData;
+        child = childParentData.nextSibling;
+        childIndex++;
+      }
+    }
+
+//    double minCrossSize = maxGrandchildrenCrossSize.isNotEmpty ? maxGrandchildrenCrossSize.values.reduce((value, element) => value + element) : 0;
+//    debugPrint('this:$this crossSize:$crossSize minChildrenMainSize:$minChildrenMainSize maxGrandchildrenCrossSize:$maxGrandchildrenCrossSize tabluarFlexDescendants:$tabluarFlexDescendants');
+    if (maxGrandchildrenCrossSize.isNotEmpty) {
+      double minCrossSize = maxGrandchildrenCrossSize.values
+          .reduce((value, element) => value + element);
+//      debugPrint('${DateTime.now().toString().substring(5, 22)} tabluar_flex.dart(624) $this.performLayout: '
+//        'minCrossSize:$minCrossSize crossSize:$crossSize minChildrenMainSize:$minChildrenMainSize maxGrandchildrenCrossSize:$maxGrandchildrenCrossSize');
+
+      child = firstChild;
+      while (child != null) {
+//          debugPrint('this:$this relayout child:$child');
+        BoxConstraints innerConstraints = childrenConstraints[child]!;
+        switch (direction) {
+          case Axis.horizontal:
+            innerConstraints = innerConstraints.copyWith(
+                minHeight: math.max(innerConstraints.minHeight, minCrossSize));
+            break;
+          case Axis.vertical:
+            innerConstraints = innerConstraints.copyWith(
+                minWidth: math.max(innerConstraints.minWidth, minCrossSize));
+            break;
+        }
+//        debugPrint('childrenConstraints[child]:${childrenConstraints[child]} innerConstraints:$innerConstraints');
+
+        if (tabluarFlexDescendants.containsKey(child)) {
+          bool callbackCalled = false;
+          void _childLayoutCallback(BoxConstraints constraints) {
+            callbackCalled = true;
+          }
+
+          RenderTabluarFlex tabluarFlexChild = tabluarFlexDescendants[child]!;
+          tabluarFlexChild.layoutCallbackQueue.addLast(_childLayoutCallback);
+          tabluarFlexChild.minMainSizesQueue.addLast(maxGrandchildrenCrossSize);
+          tabluarFlexChild.layout(innerConstraints, parentUsesSize: true);
+          child.layout(innerConstraints, parentUsesSize: true);
+          if (!callbackCalled) {
+            tabluarFlexChild.layout(innerConstraints, parentUsesSize: true);
+          }
+          tabluarFlexChild.minMainSizesQueue.removeLast();
+          tabluarFlexChild.layoutCallbackQueue.removeLast();
+        } else {
+          child.layout(innerConstraints, parentUsesSize: true);
+        }
+        crossSize = math.max(crossSize, _getCrossSize(child));
+
+        final FlexParentData childParentData =
+            child.parentData! as FlexParentData;
+        child = childParentData.nextSibling;
+      }
+//    debugPrint('this:$this updated crossSize:$crossSize');
+    }
+
+    // Align items along the main axis.
+    final double idealSize = canFlex && mainAxisSize == MainAxisSize.max
+        ? maxMainSize
+        : allocatedSize;
+    double actualSize;
+    double actualSizeDelta;
+    switch (direction) {
+      case Axis.horizontal:
+        size = constraints.constrain(Size(idealSize, crossSize));
+        actualSize = size.width;
+        crossSize = size.height;
+        break;
+      case Axis.vertical:
+        size = constraints.constrain(Size(crossSize, idealSize));
+        actualSize = size.height;
+        crossSize = size.width;
+        break;
+    }
+    actualSizeDelta = actualSize - allocatedSize;
+    _overflow = math.max(0.0, -actualSizeDelta);
+
+    final double remainingSpace = math.max(0.0, actualSizeDelta);
+    double leadingSpace;
+    double betweenSpace;
+    // flipMainAxis is used to decide whether to lay out left-to-right/top-to-bottom (false), or
+    // right-to-left/bottom-to-top (true). The _startIsTopLeft will return null if there's only
+    // one child and the relevant direction is null, in which case we arbitrarily decide not to
+    // flip, but that doesn't have any detectable effect.
+    final bool flipMainAxis =
+        !(_startIsTopLeft(direction, textDirection, verticalDirection) ?? true);
+    switch (mainAxisAlignment) {
+      case MainAxisAlignment.start:
+        leadingSpace = 0.0;
+        betweenSpace = 0.0;
+        break;
+      case MainAxisAlignment.end:
+        leadingSpace = remainingSpace;
+        betweenSpace = 0.0;
+        break;
+      case MainAxisAlignment.center:
+        leadingSpace = remainingSpace / 2.0;
+        betweenSpace = 0.0;
+        break;
+      case MainAxisAlignment.spaceBetween:
+        leadingSpace = 0.0;
+        betweenSpace =
+            totalChildren > 1 ? remainingSpace / (totalChildren - 1) : 0.0;
+        break;
+      case MainAxisAlignment.spaceAround:
+        betweenSpace = totalChildren > 0 ? remainingSpace / totalChildren : 0.0;
+        leadingSpace = betweenSpace / 2.0;
+        break;
+      case MainAxisAlignment.spaceEvenly:
+        betweenSpace =
+            totalChildren > 0 ? remainingSpace / (totalChildren + 1) : 0.0;
+        leadingSpace = betweenSpace;
+        break;
+    }
+
+    // Position elements
+    double childMainPosition =
+        flipMainAxis ? actualSize - leadingSpace : leadingSpace;
+    child = firstChild;
+    while (child != null) {
+      final FlexParentData childParentData =
+          child.parentData! as FlexParentData;
+      double childCrossPosition;
+      switch (crossAxisAlignment) {
+        case CrossAxisAlignment.start:
+        case CrossAxisAlignment.end:
+          childCrossPosition = _startIsTopLeft(
+                      flipAxis(direction), textDirection, verticalDirection) ==
+                  (crossAxisAlignment == CrossAxisAlignment.start)
+              ? 0.0
+              : crossSize - _getCrossSize(child);
+          break;
+        case CrossAxisAlignment.center:
+          childCrossPosition = crossSize / 2.0 - _getCrossSize(child) / 2.0;
+          break;
+        case CrossAxisAlignment.stretch:
+          childCrossPosition = 0.0;
+          break;
+        case CrossAxisAlignment.baseline:
+          childCrossPosition = 0.0;
+          if (direction == Axis.horizontal) {
+            assert(textBaseline != null);
+            final double? distance =
+                child.getDistanceToBaseline(textBaseline!, onlyReal: true);
+            if (distance != null)
+              childCrossPosition = maxBaselineDistance - distance;
+          }
+          break;
+      }
+      if (flipMainAxis) childMainPosition -= _getMainSize(child);
+      switch (direction) {
+        case Axis.horizontal:
+          childParentData.offset =
+              Offset(childMainPosition, childCrossPosition);
+          break;
+        case Axis.vertical:
+          childParentData.offset =
+              Offset(childCrossPosition, childMainPosition);
+          break;
+      }
+      if (flipMainAxis) {
+        childMainPosition -= betweenSpace;
+      } else {
+        childMainPosition += _getMainSize(child) + betweenSpace;
+      }
+      child = childParentData.nextSibling;
+    }
+
+    this._maxGrandchildrenCrossSize.clear();
+    this._maxGrandchildrenCrossSize.addAll(maxGrandchildrenCrossSize);
+
+    //we can only call this callback when we've done this object's layout. So that size will be valid for the callbackee.
+    if (this.layoutCallbackQueue.isNotEmpty) {
+      this
+          .layoutCallbackQueue
+          .forEach((LayoutCallback<BoxConstraints> callback) {
+        invokeLayoutCallback(callback);
+      });
+    }
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    if (!_hasOverflow) {
+      defaultPaint(context, offset);
+      return;
+    }
+
+    // There's no point in drawing the children if we're empty.
+    if (size.isEmpty) return;
+
+    if (_decoration != null) {
+      _painter ??= _decoration!.createBoxPainter(markNeedsPaint);
+      final ImageConfiguration filledConfiguration =
+          configuration.copyWith(size: size);
+      _painter!.paint(context.canvas, offset, filledConfiguration);
+    }
+
+    // We have overflow. Clip it.
+    context.pushClipRect(
+        needsCompositing, offset, Offset.zero & size, defaultPaint);
+
+//    assert(() {
+//      // Only set this if it's null to save work. It gets reset to null if the
+//      // _direction changes.
+//      final List<DiagnosticsNode> debugOverflowHints = <DiagnosticsNode>[];
+//      debugOverflowHints.add(ErrorDescription(
+//          'The overflowing $runtimeType has an orientation of $direction.\n'
+//          'The edge of the $runtimeType that is overflowing has been marked '
+//          'in the rendering with a yellow and black striped pattern. This is '
+//          'usually caused by the contents being too big for the $runtimeType. '
+//          'Consider applying a flex factor (e.g. using an Expanded widget) to '
+//          'force the children of the $runtimeType to fit within the available '
+//          'space instead of being sized to their natural size.\n'
+//      ));
+//      debugOverflowHints.add(ErrorHint(
+//          'This is considered an error condition because it indicates that there '
+//          'is content that cannot be seen. If the content is legitimately bigger '
+//          'than the available space, consider clipping it with a ClipRect widget '
+//          'before putting it in the flex, or using a scrollable container rather '
+//          'than a Flex, like a ListView.'
+//      ));
+//
+//      // Simulate a child rect that overflows by the right amount. This child
+//      // rect is never used for drawing, just for determining the overflow
+//      // location and amount.
+//      Rect overflowChildRect;
+//      switch (direction) {
+//        case Axis.horizontal:
+//          overflowChildRect =
+//              Rect.fromLTWH(0.0, 0.0, size.width + _overflow, 0.0);
+//          break;
+//        case Axis.vertical:
+//          overflowChildRect =
+//              Rect.fromLTWH(0.0, 0.0, 0.0, size.height + _overflow);
+//          break;
+//      }
+//      paintOverflowIndicator(
+//          context, offset, Offset.zero & size, overflowChildRect,
+//          overflowHints: debugOverflowHints);
+//      return true;
+//    }());
+  }
+
+  @override
+  Rect? describeApproximatePaintClip(RenderObject child) =>
+      _hasOverflow ? Offset.zero & size : null;
+
+  @override
+  String toStringShort() {
+    String header = super.toStringShort();
+    if (_hasOverflow) header += ' OVERFLOWING';
+    return header;
+  }
+}

+ 73 - 0
plugins/reorderables/lib/src/rendering/transitions.dart

@@ -0,0 +1,73 @@
+import 'package:flutter/rendering.dart';
+import 'package:flutter/animation.dart';
+
+class RenderSizeTransitionWithIntrinsicSize extends RenderProxyBox {
+  RenderSizeTransitionWithIntrinsicSize({
+    this.axis = Axis.vertical,
+    required this.sizeFactor,
+    RenderBox? child,
+  })  :
+//       _axis = axis,
+//       _sizeFactor = sizeFactor,
+        super(child);
+
+  Axis axis;
+//  Axis get axis => _axis;
+//  set axis(Axis value) {
+//    _axis = value;
+//  }
+
+  Animation<double> sizeFactor;
+//  Animation<double> get sizeFactor => _sizeFactor;
+//  set sizeFactor(Animation<double> value) {
+//    _sizeFactor = value;
+//  }
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    final child = this.child;
+    if (child != null) {
+      double childWidth = child.getMinIntrinsicWidth(height);
+      return axis == Axis.horizontal
+          ? childWidth * sizeFactor.value
+          : childWidth;
+    }
+    return 0.0;
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    final child = this.child;
+    if (child != null) {
+      double childWidth = child.getMaxIntrinsicWidth(height);
+      return axis == Axis.horizontal
+          ? childWidth * sizeFactor.value
+          : childWidth;
+    }
+    return 0.0;
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    final child = this.child;
+    if (child != null) {
+      double childHeight = child.getMinIntrinsicHeight(width);
+      return axis == Axis.vertical
+          ? childHeight * sizeFactor.value
+          : childHeight;
+    }
+    return 0.0;
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    final child = this.child;
+    if (child != null) {
+      double childHeight = child.getMaxIntrinsicHeight(width);
+      return axis == Axis.vertical
+          ? childHeight * sizeFactor.value
+          : childHeight;
+    }
+    return 0.0;
+  }
+}

+ 505 - 0
plugins/reorderables/lib/src/rendering/wrap.dart

@@ -0,0 +1,505 @@
+import 'dart:math' as math;
+
+import 'package:flutter/rendering.dart';
+
+class _RunMetrics {
+  _RunMetrics(this.mainAxisExtent, this.crossAxisExtent, this.childCount);
+
+  final double mainAxisExtent;
+  final double crossAxisExtent;
+  final int childCount;
+}
+
+typedef _ChildSizingFunction = double Function(RenderBox child, double extent);
+
+/// Parent data for use with [RenderWrap].
+class WrapWithMainAxisCountParentData extends WrapParentData {
+  int _runIndex = 0;
+}
+
+class RenderWrapWithMainAxisCount extends RenderWrap {
+  RenderWrapWithMainAxisCount({
+    List<RenderBox>? children,
+    Axis direction = Axis.horizontal,
+    WrapAlignment alignment = WrapAlignment.start,
+    double spacing = 0.0,
+    WrapAlignment runAlignment = WrapAlignment.start,
+    double runSpacing = 0.0,
+    WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start,
+    TextDirection? textDirection,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    this.minMainAxisCount,
+    this.maxMainAxisCount,
+  })  : assert(minMainAxisCount == null ||
+            maxMainAxisCount == null ||
+            maxMainAxisCount >= minMainAxisCount),
+//       _minMainAxisCount = minMainAxisCount,
+//       _maxMainAxisCount = maxMainAxisCount,
+        super(
+          children: children,
+          direction: direction,
+          alignment: alignment,
+          spacing: spacing,
+          runAlignment: runAlignment,
+          runSpacing: runSpacing,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+        );
+
+  int? minMainAxisCount;
+
+//  int get minMainAxisCount => _minMainAxisCount;
+//  set minMainAxisCount(int value) {
+//    _minMainAxisCount = value;
+//  }
+
+  int? maxMainAxisCount;
+
+//  int get maxMainAxisCount => _maxMainAxisCount;
+//  set maxMainAxisCount(int value) {
+//    _maxMainAxisCount = value;
+//  }
+
+  bool get _debugHasNecessaryDirections {
+    if (firstChild != null && lastChild != firstChild) {
+      // i.e. there's more than one child
+      assert(
+          direction == Axis.vertical || textDirection != null,
+          'Horizontal $runtimeType with multiple children has a null '
+          'textDirection, so the layout order is undefined.');
+    }
+    if (alignment == WrapAlignment.start || alignment == WrapAlignment.end) {
+      assert(
+          direction == Axis.vertical || textDirection != null,
+          'Horizontal $runtimeType with alignment $alignment has a null '
+          'textDirection, so the alignment cannot be resolved.');
+    }
+    if (runAlignment == WrapAlignment.start ||
+        runAlignment == WrapAlignment.end) {
+      assert(
+          direction == Axis.horizontal || textDirection != null,
+          'Horizontal $runtimeType with runAlignment $runAlignment has a null '
+          'verticalDirection, so the alignment cannot be resolved.');
+    }
+    if (crossAxisAlignment == WrapCrossAlignment.start ||
+        crossAxisAlignment == WrapCrossAlignment.end) {
+      assert(
+          direction == Axis.horizontal || textDirection != null,
+          'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment '
+          'has a null textDirection, so the alignment cannot be resolved.');
+    }
+    return true;
+  }
+
+  @override
+  void setupParentData(RenderBox child) {
+    if (child.parentData is! WrapWithMainAxisCountParentData)
+      child.parentData = WrapWithMainAxisCountParentData();
+  }
+
+  double _computeIntrinsicHeightForWidth(double width) {
+    assert(direction == Axis.horizontal);
+    int runCount = 0;
+    double height = 0.0;
+    double runWidth = 0.0;
+    double runHeight = 0.0;
+    int childCount = 0;
+    RenderBox? child = firstChild;
+    int minChildCount = minMainAxisCount ?? 1;
+    int maxChildCount = maxMainAxisCount ?? -1;
+    while (child != null) {
+      final double childWidth = child.getMaxIntrinsicWidth(double.infinity);
+      final double childHeight = child.getMaxIntrinsicHeight(childWidth);
+      //the number of children per row/column (run) must be equal/larger than minChildCount and equal/smaller than maxChildCount.
+      if (childCount >= minChildCount &&
+          (runWidth + childWidth > width ||
+              (maxChildCount >= minChildCount &&
+                  childCount >= maxChildCount))) {
+        height += runHeight;
+        if (runCount > 0) height += runSpacing;
+        runCount += 1;
+        runWidth = 0.0;
+        runHeight = 0.0;
+        childCount = 0;
+      }
+      runWidth += childWidth;
+      runHeight = math.max(runHeight, childHeight);
+      if (childCount > 0) runWidth += spacing;
+      childCount += 1;
+      child = childAfter(child);
+    }
+    if (childCount > 0) height += runHeight + runSpacing;
+    return height;
+  }
+
+  double _computeIntrinsicWidthForHeight(double height) {
+    assert(direction == Axis.vertical);
+    int runCount = 0;
+    double width = 0.0;
+    double runHeight = 0.0;
+    double runWidth = 0.0;
+    int childCount = 0;
+    RenderBox? child = firstChild;
+    int minChildCount = minMainAxisCount ?? 1;
+    int maxChildCount = maxMainAxisCount ?? -1;
+    while (child != null) {
+      final double childHeight = child.getMaxIntrinsicHeight(double.infinity);
+      final double childWidth = child.getMaxIntrinsicWidth(childHeight);
+      //the number of children per row/column (run) must be equal/larger than minChildCount and equal/smaller than maxChildCount.
+      if (childCount >= minChildCount &&
+          (runHeight + childHeight > height ||
+              (maxChildCount >= minChildCount &&
+                  childCount >= maxChildCount))) {
+        width += runWidth;
+        if (runCount > 0) width += runSpacing;
+        runCount += 1;
+        runHeight = 0.0;
+        runWidth = 0.0;
+        childCount = 0;
+      }
+      runHeight += childHeight;
+      runWidth = math.max(runWidth, childWidth);
+      if (childCount > 0) runHeight += spacing;
+      childCount += 1;
+      child = childAfter(child);
+    }
+    if (childCount > 0) width += runWidth + runSpacing;
+    return width;
+  }
+
+  double _getIntrinsicSize(
+      {
+//    Axis sizingDirection,
+//    double extent, // the extent in the direction that isn't the sizing direction
+      required int childCountAlongMainAxis,
+      required _ChildSizingFunction
+          childSize // a method to find the size in the sizing direction
+      }) {
+    double runMainAxisExtent = 0.0;
+    double maxRunMainAxisExtent = 0.0;
+    int childCount = 0;
+    RenderBox? child = firstChild;
+//    final List<double> runMainAxisExtents = [];
+    while (child != null) {
+      final double childMainAxisExtent = childSize(child, double.infinity);
+      if (childCountAlongMainAxis > 0 &&
+          childCount >= childCountAlongMainAxis) {
+        maxRunMainAxisExtent =
+            math.max(maxRunMainAxisExtent, runMainAxisExtent);
+        runMainAxisExtent = 0.0;
+        childCount = 0;
+      }
+      runMainAxisExtent += childMainAxisExtent;
+      if (childCount > 0) runMainAxisExtent += spacing;
+      childCount += 1;
+      child = childAfter(child);
+    }
+    if (childCount > 0)
+      maxRunMainAxisExtent = math.max(maxRunMainAxisExtent, runMainAxisExtent);
+    return maxRunMainAxisExtent;
+  }
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    switch (direction) {
+      case Axis.horizontal:
+        return _getIntrinsicSize(
+            childCountAlongMainAxis: minMainAxisCount ?? 1,
+            childSize: (RenderBox child, double extent) =>
+                child.getMinIntrinsicWidth(extent));
+      case Axis.vertical:
+        return _computeIntrinsicWidthForHeight(height);
+    }
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    switch (direction) {
+      case Axis.horizontal:
+        return _getIntrinsicSize(
+            childCountAlongMainAxis: maxMainAxisCount ?? -1,
+            childSize: (RenderBox child, double extent) =>
+                child.getMaxIntrinsicWidth(extent));
+      case Axis.vertical:
+        return _computeIntrinsicWidthForHeight(height);
+    }
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    switch (direction) {
+      case Axis.horizontal:
+        return _computeIntrinsicHeightForWidth(width);
+      case Axis.vertical:
+        return _getIntrinsicSize(
+            childCountAlongMainAxis: minMainAxisCount ?? 1,
+            childSize: (RenderBox child, double extent) =>
+                child.getMinIntrinsicHeight(extent));
+    }
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    switch (direction) {
+      case Axis.horizontal:
+        return _computeIntrinsicHeightForWidth(width);
+      case Axis.vertical:
+        return _getIntrinsicSize(
+            childCountAlongMainAxis: maxMainAxisCount ?? -1,
+            childSize: (RenderBox child, double extent) =>
+                child.getMaxIntrinsicHeight(extent));
+    }
+  }
+
+  double _getMainAxisExtent(RenderBox child) {
+    switch (direction) {
+      case Axis.horizontal:
+        return child.size.width;
+      case Axis.vertical:
+        return child.size.height;
+    }
+  }
+
+  double _getCrossAxisExtent(RenderBox child) {
+    switch (direction) {
+      case Axis.horizontal:
+        return child.size.height;
+      case Axis.vertical:
+        return child.size.width;
+    }
+  }
+
+  Offset _getOffset(double mainAxisOffset, double crossAxisOffset) {
+    switch (direction) {
+      case Axis.horizontal:
+        return Offset(mainAxisOffset, crossAxisOffset);
+      case Axis.vertical:
+        return Offset(crossAxisOffset, mainAxisOffset);
+    }
+  }
+
+  double _getChildCrossAxisOffset(bool flipCrossAxis, double runCrossAxisExtent,
+      double childCrossAxisExtent) {
+    final double freeSpace = runCrossAxisExtent - childCrossAxisExtent;
+    switch (crossAxisAlignment) {
+      case WrapCrossAlignment.start:
+        return flipCrossAxis ? freeSpace : 0.0;
+      case WrapCrossAlignment.end:
+        return flipCrossAxis ? 0.0 : freeSpace;
+      case WrapCrossAlignment.center:
+        return freeSpace / 2.0;
+    }
+  }
+
+  bool _hasVisualOverflow = false;
+  late List<int> childRunIndexes;
+
+  @override
+  void performLayout() {
+    assert(_debugHasNecessaryDirections);
+    _hasVisualOverflow = false;
+    RenderBox? child = firstChild;
+    if (child == null) {
+      size = constraints.smallest;
+      return;
+    }
+    BoxConstraints childConstraints;
+    double mainAxisLimit = 0.0;
+    bool flipMainAxis = false;
+    bool flipCrossAxis = false;
+    switch (direction) {
+      case Axis.horizontal:
+        childConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
+        mainAxisLimit = constraints.maxWidth;
+        if (textDirection == TextDirection.rtl) flipMainAxis = true;
+        if (verticalDirection == VerticalDirection.up) flipCrossAxis = true;
+        break;
+      case Axis.vertical:
+        childConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
+        mainAxisLimit = constraints.maxHeight;
+        if (verticalDirection == VerticalDirection.up) flipMainAxis = true;
+        if (textDirection == TextDirection.rtl) flipCrossAxis = true;
+        break;
+    }
+    final double spacing = this.spacing;
+    final double runSpacing = this.runSpacing;
+    final List<_RunMetrics> runMetrics = <_RunMetrics>[];
+    double mainAxisExtent = 0.0;
+    double crossAxisExtent = 0.0;
+    double runMainAxisExtent = 0.0;
+    double runCrossAxisExtent = 0.0;
+    int childCount = 0;
+    int minChildCount = minMainAxisCount ?? 1;
+    int maxChildCount = maxMainAxisCount ?? -1;
+    int runIndex = 0;
+    childRunIndexes = [];
+    while (child != null) {
+      child.layout(childConstraints, parentUsesSize: true);
+      final double childMainAxisExtent = _getMainAxisExtent(child);
+      final double childCrossAxisExtent = _getCrossAxisExtent(child);
+      //if (childCount > 0 && runMainAxisExtent + spacing + childMainAxisExtent > mainAxisLimit) {
+      if (childCount >= minChildCount &&
+          (runMainAxisExtent + spacing + childMainAxisExtent > mainAxisLimit ||
+              (maxChildCount >= minChildCount &&
+                  childCount >= maxChildCount))) {
+        mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
+        crossAxisExtent += runCrossAxisExtent;
+        if (runMetrics.isNotEmpty) crossAxisExtent += runSpacing;
+        runMetrics.add(
+            _RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount));
+        runMainAxisExtent = 0.0;
+        runCrossAxisExtent = 0.0;
+        childCount = 0;
+        runIndex++;
+      }
+      runMainAxisExtent += childMainAxisExtent;
+      if (childCount > 0) runMainAxisExtent += spacing;
+      runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent);
+      childCount += 1;
+      final WrapWithMainAxisCountParentData childParentData =
+          child.parentData! as WrapWithMainAxisCountParentData;
+      childParentData._runIndex = runMetrics.length;
+      child = childParentData.nextSibling;
+      childRunIndexes.add(runIndex);
+    }
+    if (childCount > 0) {
+      mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
+      crossAxisExtent += runCrossAxisExtent;
+      if (runMetrics.isNotEmpty) crossAxisExtent += runSpacing;
+      runMetrics
+          .add(_RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount));
+    }
+
+    final int runCount = runMetrics.length;
+    assert(runCount > 0);
+
+    double containerMainAxisExtent = 0.0;
+    double containerCrossAxisExtent = 0.0;
+
+    switch (direction) {
+      case Axis.horizontal:
+        size = constraints.constrain(Size(mainAxisExtent, crossAxisExtent));
+        containerMainAxisExtent = size.width;
+        containerCrossAxisExtent = size.height;
+        break;
+      case Axis.vertical:
+        size = constraints.constrain(Size(crossAxisExtent, mainAxisExtent));
+        containerMainAxisExtent = size.height;
+        containerCrossAxisExtent = size.width;
+        break;
+    }
+
+    _hasVisualOverflow = containerMainAxisExtent < mainAxisExtent ||
+        containerCrossAxisExtent < crossAxisExtent;
+
+    final double crossAxisFreeSpace =
+        math.max(0.0, containerCrossAxisExtent - crossAxisExtent);
+    double runLeadingSpace = 0.0;
+    double runBetweenSpace = 0.0;
+    switch (runAlignment) {
+      case WrapAlignment.start:
+        break;
+      case WrapAlignment.end:
+        runLeadingSpace = crossAxisFreeSpace;
+        break;
+      case WrapAlignment.center:
+        runLeadingSpace = crossAxisFreeSpace / 2.0;
+        break;
+      case WrapAlignment.spaceBetween:
+        runBetweenSpace =
+            runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0;
+        break;
+      case WrapAlignment.spaceAround:
+        runBetweenSpace = crossAxisFreeSpace / runCount;
+        runLeadingSpace = runBetweenSpace / 2.0;
+        break;
+      case WrapAlignment.spaceEvenly:
+        runBetweenSpace = crossAxisFreeSpace / (runCount + 1);
+        runLeadingSpace = runBetweenSpace;
+        break;
+    }
+
+    runBetweenSpace += runSpacing;
+    double crossAxisOffset = flipCrossAxis
+        ? containerCrossAxisExtent - runLeadingSpace
+        : runLeadingSpace;
+
+    child = firstChild;
+    for (int i = 0; i < runCount; ++i) {
+      final _RunMetrics metrics = runMetrics[i];
+      final double runMainAxisExtent = metrics.mainAxisExtent;
+      final double runCrossAxisExtent = metrics.crossAxisExtent;
+      final int childCount = metrics.childCount;
+
+      final double mainAxisFreeSpace =
+          math.max(0.0, containerMainAxisExtent - runMainAxisExtent);
+      double childLeadingSpace = 0.0;
+      double childBetweenSpace = 0.0;
+
+      switch (alignment) {
+        case WrapAlignment.start:
+          break;
+        case WrapAlignment.end:
+          childLeadingSpace = mainAxisFreeSpace;
+          break;
+        case WrapAlignment.center:
+          childLeadingSpace = mainAxisFreeSpace / 2.0;
+          break;
+        case WrapAlignment.spaceBetween:
+          childBetweenSpace =
+              childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0;
+          break;
+        case WrapAlignment.spaceAround:
+          childBetweenSpace = mainAxisFreeSpace / childCount;
+          childLeadingSpace = childBetweenSpace / 2.0;
+          break;
+        case WrapAlignment.spaceEvenly:
+          childBetweenSpace = mainAxisFreeSpace / (childCount + 1);
+          childLeadingSpace = childBetweenSpace;
+          break;
+      }
+
+      childBetweenSpace += spacing;
+      double childMainPosition = flipMainAxis
+          ? containerMainAxisExtent - childLeadingSpace
+          : childLeadingSpace;
+
+      if (flipCrossAxis) crossAxisOffset -= runCrossAxisExtent;
+
+      while (child != null) {
+        final WrapWithMainAxisCountParentData childParentData =
+            child.parentData! as WrapWithMainAxisCountParentData;
+        if (childParentData._runIndex != i) break;
+        final double childMainAxisExtent = _getMainAxisExtent(child);
+        final double childCrossAxisExtent = _getCrossAxisExtent(child);
+        final double childCrossAxisOffset = _getChildCrossAxisOffset(
+            flipCrossAxis, runCrossAxisExtent, childCrossAxisExtent);
+        if (flipMainAxis) childMainPosition -= childMainAxisExtent;
+        childParentData.offset = _getOffset(
+            childMainPosition, crossAxisOffset + childCrossAxisOffset);
+        if (flipMainAxis)
+          childMainPosition -= childBetweenSpace;
+        else
+          childMainPosition += childMainAxisExtent + childBetweenSpace;
+        child = childParentData.nextSibling;
+      }
+
+      if (flipCrossAxis)
+        crossAxisOffset -= runBetweenSpace;
+      else
+        crossAxisOffset += runCrossAxisExtent + runBetweenSpace;
+    }
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    // TODO(ianh): move the debug flex overflow paint logic somewhere common so
+    // it can be reused here
+    if (_hasVisualOverflow)
+      context.pushClipRect(
+          needsCompositing, offset, Offset.zero & size, defaultPaint);
+    else
+      defaultPaint(context, offset);
+  }
+}

+ 36 - 0
plugins/reorderables/lib/src/widgets/basic.dart

@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+
+import 'safe_state.dart';
+
+/// A platonic widget that both has state and calls a closure to obtain its child widget.
+///
+/// See also:
+///
+///  * [Builder], the platonic stateless widget.
+class SafeStatefulBuilder extends StatefulWidget {
+  /// Creates a widget that both has state and delegates its build to a callback.
+  ///
+  /// The [builder] argument must not be null.
+  const SafeStatefulBuilder({
+    required this.builder,
+    Key? key,
+  }) : super(key: key);
+
+  /// Called to obtain the child widget.
+  ///
+  /// This function is called whenever this widget is included in its parent's
+  /// build and the old widget (if any) that it synchronizes with has a distinct
+  /// object identity. Typically the parent's build method will construct
+  /// a new tree of widgets and so a new Builder child will not be [identical]
+  /// to the corresponding old one.
+  final StatefulWidgetBuilder builder;
+
+  @override
+  _SafeStatefulBuilderState createState() => _SafeStatefulBuilderState();
+}
+
+class _SafeStatefulBuilderState extends State<SafeStatefulBuilder>
+    with SafeStateMixin<SafeStatefulBuilder> {
+  @override
+  Widget build(BuildContext context) => widget.builder(context, setState);
+}

+ 601 - 0
plugins/reorderables/lib/src/widgets/passthrough_overlay.dart

@@ -0,0 +1,601 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/scheduler.dart';
+
+//import 'basic.dart';
+//import 'debug.dart';
+//import 'framework.dart';
+//import 'ticker_provider.dart';
+
+/// A place in an [Overlay] that can contain a widget.
+///
+/// Overlay entries are inserted into an [Overlay] using the
+/// [OverlayState.insert] or [OverlayState.insertAll] functions. To find the
+/// closest enclosing overlay for a given [BuildContext], use the [Overlay.of]
+/// function.
+///
+/// An overlay entry can be in at most one overlay at a time. To remove an entry
+/// from its overlay, call the [remove] function on the overlay entry.
+///
+/// Because an [Overlay] uses a [Stack] layout, overlay entries can use
+/// [Positioned] and [AnimatedPositioned] to position themselves within the
+/// overlay.
+///
+/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
+/// follows the user's finger across the screen after the drag begins. Using the
+/// overlay to display the drag avatar lets the avatar float over the other
+/// widgets in the app. As the user's finger moves, draggable calls
+/// [markNeedsBuild] on the overlay entry to cause it to rebuild. It its build,
+/// the entry includes a [Positioned] with its top and left property set to
+/// position the drag avatar near the user's finger. When the drag is over,
+/// [Draggable] removes the entry from the overlay to remove the drag avatar
+/// from view.
+///
+/// By default, if there is an entirely [opaque] entry over this one, then this
+/// one will not be included in the widget tree (in particular, stateful widgets
+/// within the overlay entry will not be instantiated). To ensure that your
+/// overlay entry is still built even if it is not visible, set [maintainState]
+/// to true. This is more expensive, so should be done with care. In particular,
+/// if widgets in an overlay entry with [maintainState] set to true repeatedly
+/// call [State.setState], the user's battery will be drained unnecessarily.
+///
+/// See also:
+///
+///  * [Overlay]
+///  * [OverlayState]
+///  * [WidgetsApp]
+///  * [MaterialApp]
+class PassthroughOverlayEntry {
+  /// Creates an overlay entry.
+  ///
+  /// To insert the entry into an [Overlay], first find the overlay using
+  /// [Overlay.of] and then call [OverlayState.insert]. To remove the entry,
+  /// call [remove] on the overlay entry itself.
+  PassthroughOverlayEntry({
+    required this.builder,
+    bool opaque = false,
+    bool maintainState = false,
+  })  : _opaque = opaque,
+        _maintainState = maintainState;
+
+  /// This entry will include the widget built by this builder in the overlay at
+  /// the entry's position.
+  ///
+  /// To cause this builder to be called again, call [markNeedsBuild] on this
+  /// overlay entry.
+  final WidgetBuilder builder;
+
+  /// Whether this entry occludes the entire overlay.
+  ///
+  /// If an entry claims to be opaque, then, for efficiency, the overlay will
+  /// skip building entries below that entry unless they have [maintainState]
+  /// set.
+  bool get opaque => _opaque;
+  bool _opaque;
+
+  set opaque(bool value) {
+    if (_opaque == value) return;
+    _opaque = value;
+    assert(_overlay != null);
+    _overlay!._didChangeEntryOpacity();
+  }
+
+  /// Whether this entry must be included in the tree even if there is a fully
+  /// [opaque] entry above it.
+  ///
+  /// By default, if there is an entirely [opaque] entry over this one, then this
+  /// one will not be included in the widget tree (in particular, stateful widgets
+  /// within the overlay entry will not be instantiated). To ensure that your
+  /// overlay entry is still built even if it is not visible, set [maintainState]
+  /// to true. This is more expensive, so should be done with care. In particular,
+  /// if widgets in an overlay entry with [maintainState] set to true repeatedly
+  /// call [State.setState], the user's battery will be drained unnecessarily.
+  ///
+  /// This is used by the [Navigator] and [Route] objects to ensure that routes
+  /// are kept around even when in the background, so that [Future]s promised
+  /// from subsequent routes will be handled properly when they complete.
+  bool get maintainState => _maintainState;
+  bool _maintainState;
+
+  set maintainState(bool value) {
+    if (_maintainState == value) return;
+    _maintainState = value;
+    assert(_overlay != null);
+    _overlay!._didChangeEntryOpacity();
+  }
+
+  PassthroughOverlayState? _overlay;
+  final GlobalKey<_OverlayEntryState> _key = GlobalKey<_OverlayEntryState>();
+
+  /// Remove this entry from the overlay.
+  ///
+  /// This should only be called once.
+  ///
+  /// If this method is called while the [SchedulerBinding.schedulerPhase] is
+  /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
+  /// paint phases (see [WidgetsBinding.drawFrame]), then the removal is
+  /// delayed until the post-frame callbacks phase. Otherwise the removal is
+  /// done synchronously. This means that it is safe to call during builds, but
+  /// also that if you do call this during a build, the UI will not update until
+  /// the next frame (i.e. many milliseconds later).
+  void remove() {
+    assert(_overlay != null);
+    final PassthroughOverlayState overlay = _overlay!;
+    _overlay = null;
+    if (SchedulerBinding.instance.schedulerPhase ==
+        SchedulerPhase.persistentCallbacks) {
+      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
+        overlay._remove(this);
+      });
+    } else {
+      overlay._remove(this);
+    }
+  }
+
+  /// Cause this entry to rebuild during the next pipeline flush.
+  ///
+  /// You need to call this function if the output of [builder] has changed.
+  void markNeedsBuild() {
+    _key.currentState?._markNeedsBuild();
+  }
+
+  @override
+  String toString() =>
+      '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
+}
+
+class _OverlayEntry extends StatefulWidget {
+  _OverlayEntry(this.entry) : super(key: entry._key);
+
+  final PassthroughOverlayEntry entry;
+
+  @override
+  _OverlayEntryState createState() => _OverlayEntryState();
+}
+
+class _OverlayEntryState extends State<_OverlayEntry> {
+  @override
+  Widget build(BuildContext context) {
+    return widget.entry.builder(context);
+  }
+
+  void _markNeedsBuild() {
+    setState(() {
+      /* the state that changed is in the builder */
+    });
+  }
+}
+
+/// A [Stack] of entries that can be managed independently.
+///
+/// Overlays let independent child widgets "float" visual elements on top of
+/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
+/// each of these widgets manage their participation in the overlay using
+/// [OverlayEntry] objects.
+///
+/// Although you can create an [Overlay] directly, it's most common to use the
+/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
+/// navigator uses its overlay to manage the visual appearance of its routes.
+///
+/// See also:
+///
+///  * [OverlayEntry].
+///  * [OverlayState].
+///  * [WidgetsApp].
+///  * [MaterialApp].
+class PassthroughOverlay extends StatefulWidget {
+  /// Creates an overlay.
+  ///
+  /// The initial entries will be inserted into the overlay when its associated
+  /// [OverlayState] is initialized.
+  ///
+  /// Rather than creating an overlay, consider using the overlay that is
+  /// created by the [WidgetsApp] or the [MaterialApp] for the application.
+  const PassthroughOverlay({
+    this.initialEntries = const <PassthroughOverlayEntry>[],
+    Key? key,
+  }) : super(key: key);
+
+  /// The entries to include in the overlay initially.
+  ///
+  /// These entries are only used when the [OverlayState] is initialized. If you
+  /// are providing a new [Overlay] description for an overlay that's already in
+  /// the tree, then the new entries are ignored.
+  ///
+  /// To add entries to an [Overlay] that is already in the tree, use
+  /// [Overlay.of] to obtain the [OverlayState] (or assign a [GlobalKey] to the
+  /// [Overlay] widget and obtain the [OverlayState] via
+  /// [GlobalKey.currentState]), and then use [OverlayState.insert] or
+  /// [OverlayState.insertAll].
+  ///
+  /// To remove an entry from an [Overlay], use [OverlayEntry.remove].
+  final List<PassthroughOverlayEntry> initialEntries;
+
+  /// The state from the closest instance of this class that encloses the given context.
+  ///
+  /// In checked mode, if the [debugRequiredFor] argument is provided then this
+  /// function will assert that an overlay was found and will throw an exception
+  /// if not. The exception attempts to explain that the calling [Widget] (the
+  /// one given by the [debugRequiredFor] argument) needs an [Overlay] to be
+  /// present to function.
+  ///
+  /// Typical usage is as follows:
+  ///
+  /// ```dart
+  /// OverlayState overlay = Overlay.of(context);
+  /// ```
+  static PassthroughOverlayState of(BuildContext context,
+      {Widget? debugRequiredFor}) {
+    final PassthroughOverlayState? result =
+        context.findAncestorStateOfType<PassthroughOverlayState>();
+    assert(() {
+      if (debugRequiredFor != null && result == null) {
+        final String additional = context.widget != debugRequiredFor
+            ? '\nThe context from which that widget was searching for an overlay was:\n  $context'
+            : '';
+        throw FlutterError('No Overlay widget found.\n'
+            '${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.\n'
+            'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.\n'
+            'The specific widget that failed to find an overlay was:\n'
+            '  $debugRequiredFor'
+            '$additional');
+      }
+      return true;
+    }());
+    return result!;
+  }
+
+  @override
+  PassthroughOverlayState createState() => PassthroughOverlayState();
+}
+
+/// The current state of an [Overlay].
+///
+/// Used to insert [OverlayEntry]s into the overlay using the [insert] and
+/// [insertAll] functions.
+class PassthroughOverlayState extends State<PassthroughOverlay>
+    with TickerProviderStateMixin {
+  final List<PassthroughOverlayEntry> _entries = <PassthroughOverlayEntry>[];
+
+  @override
+  void initState() {
+    super.initState();
+    insertAll(widget.initialEntries);
+  }
+
+  /// Insert the given entry into the overlay.
+  ///
+  /// If [above] is non-null, the entry is inserted just above [above].
+  /// Otherwise, the entry is inserted on top.
+  void insert(PassthroughOverlayEntry entry, {PassthroughOverlayEntry? above}) {
+    assert(entry._overlay == null);
+    assert(
+        above == null || (above._overlay == this && _entries.contains(above)));
+    entry._overlay = this;
+    setState(() {
+      final int index =
+          above == null ? _entries.length : _entries.indexOf(above) + 1;
+      _entries.insert(index, entry);
+    });
+  }
+
+  /// Insert all the entries in the given iterable.
+  ///
+  /// If [above] is non-null, the entries are inserted just above [above].
+  /// Otherwise, the entries are inserted on top.
+  void insertAll(Iterable<PassthroughOverlayEntry> entries,
+      {PassthroughOverlayEntry? above}) {
+    assert(
+        above == null || (above._overlay == this && _entries.contains(above)));
+    if (entries.isEmpty) return;
+    for (PassthroughOverlayEntry entry in entries) {
+      assert(entry._overlay == null);
+      entry._overlay = this;
+    }
+    setState(() {
+      final int index =
+          above == null ? _entries.length : _entries.indexOf(above) + 1;
+      _entries.insertAll(index, entries);
+    });
+  }
+
+  void _remove(PassthroughOverlayEntry entry) {
+    if (mounted) {
+      _entries.remove(entry);
+      setState(() {
+        /* entry was removed */
+      });
+    }
+  }
+
+  /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
+  /// opaque entry).
+  ///
+  /// This is an O(N) algorithm, and should not be necessary except for debug
+  /// asserts. To avoid people depending on it, this function is implemented
+  /// only in checked mode.
+  bool debugIsVisible(PassthroughOverlayEntry entry) {
+    bool result = false;
+    assert(_entries.contains(entry));
+    assert(() {
+      for (int i = _entries.length - 1; i > 0; i -= 1) {
+        final PassthroughOverlayEntry candidate = _entries[i];
+        if (candidate == entry) {
+          result = true;
+          break;
+        }
+        if (candidate.opaque) break;
+      }
+      return true;
+    }());
+    return result;
+  }
+
+  void _didChangeEntryOpacity() {
+    setState(() {
+      // We use the opacity of the entry in our build function, which means we
+      // our state has changed.
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // These lists are filled backwards. For the offstage children that
+    // does not matter since they aren't rendered, but for the onstage
+    // children we reverse the list below before adding it to the tree.
+    final List<Widget> onstageChildren = <Widget>[];
+    final List<Widget> offstageChildren = <Widget>[];
+    bool onstage = true;
+    for (int i = _entries.length - 1; i >= 0; i -= 1) {
+      final PassthroughOverlayEntry entry = _entries[i];
+      if (onstage) {
+        onstageChildren.add(_OverlayEntry(entry));
+        if (entry.opaque) onstage = false;
+      } else if (entry.maintainState) {
+        offstageChildren
+            .add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
+      }
+    }
+    return _Theatre(
+      onstage: Stack(
+        fit: StackFit.passthrough,
+        //HanSheng changed it to passthrough so that this widget doesn't change layout constraints
+        children: onstageChildren.reversed.toList(growable: false),
+      ),
+      offstage: offstageChildren,
+    );
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    // TODO(jacobr): use IterableProperty instead as that would
+    // provide a slightly more consistent string summary of the List.
+    properties.add(DiagnosticsProperty<List<PassthroughOverlayEntry>>(
+        'entries', _entries));
+  }
+}
+
+/// A widget that has one [onstage] child which is visible, and one or more
+/// [offstage] widgets which are kept alive, and are built, but are not laid out
+/// or painted.
+///
+/// The onstage widget must be a Stack.
+///
+/// For convenience, it is legal to use [Positioned] widgets around the offstage
+/// widgets.
+class _Theatre extends RenderObjectWidget {
+  _Theatre({
+    required this.offstage,
+    this.onstage,
+  });
+
+  final Stack? onstage;
+
+  final List<Widget> offstage;
+
+  @override
+  _TheatreElement createElement() => _TheatreElement(this);
+
+  @override
+  _RenderTheatre createRenderObject(BuildContext context) => _RenderTheatre();
+}
+
+class _TheatreElement extends RenderObjectElement {
+  _TheatreElement(_Theatre widget)
+      : assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage)),
+        super(widget);
+
+  @override
+  _Theatre get widget => super.widget as _Theatre;
+
+  @override
+  _RenderTheatre get renderObject => super.renderObject as _RenderTheatre;
+
+  Element? _onstage;
+  static final Object _onstageSlot = Object();
+
+  late List<Element> _offstage;
+  final Set<Element> _forgottenOffstageChildren = HashSet<Element>();
+
+  @override
+  void insertRenderObjectChild(RenderBox child, dynamic slot) {
+    assert(renderObject.debugValidateChild(child));
+    if (slot == _onstageSlot) {
+      assert(child is RenderStack);
+      renderObject.child = child as RenderStack?;
+    } else {
+      assert(slot == null || slot is Element);
+      renderObject.insert(child, after: slot?.renderObject);
+    }
+  }
+
+  @override
+  void moveRenderObjectChild(RenderBox child, dynamic oldSlot, dynamic slot) {
+    if (slot == _onstageSlot) {
+      renderObject.remove(child);
+      assert(child is RenderStack);
+      renderObject.child = child as RenderStack?;
+    } else {
+      assert(slot == null || slot is Element);
+      if (renderObject.child == child) {
+        renderObject.child = null;
+        renderObject.insert(child, after: slot?.renderObject);
+      } else {
+        renderObject.move(child, after: slot?.renderObject);
+      }
+    }
+  }
+
+  @override
+  void removeRenderObjectChild(RenderBox child, dynamic slot) {
+    if (renderObject.child == child) {
+      renderObject.child = null;
+    } else {
+      renderObject.remove(child);
+    }
+  }
+
+  @override
+  void visitChildren(ElementVisitor visitor) {
+    if (_onstage != null) visitor(_onstage!);
+    for (Element child in _offstage) {
+      if (!_forgottenOffstageChildren.contains(child)) visitor(child);
+    }
+  }
+
+  @override
+  void debugVisitOnstageChildren(ElementVisitor visitor) {
+    if (_onstage != null) visitor(_onstage!);
+  }
+
+  @override
+  void forgetChild(Element child) {
+    if (child == _onstage) {
+      _onstage = null;
+    } else {
+      assert(_offstage.contains(child));
+      assert(!_forgottenOffstageChildren.contains(child));
+      _forgottenOffstageChildren.add(child);
+    }
+    super.forgetChild(child);
+  }
+
+  @override
+  void mount(Element? parent, dynamic newSlot) {
+    super.mount(parent, newSlot);
+    _onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
+    _offstage = [];
+    Element? previousChild;
+    for (int i = 0; i < _offstage.length; i += 1) {
+      final Element newChild = inflateWidget(widget.offstage[i], previousChild);
+      _offstage[i] = newChild;
+      previousChild = newChild;
+    }
+  }
+
+  @override
+  void update(_Theatre newWidget) {
+    super.update(newWidget);
+    assert(widget == newWidget);
+    _onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
+    _offstage = updateChildren(_offstage, widget.offstage,
+        forgottenChildren: _forgottenOffstageChildren);
+    _forgottenOffstageChildren.clear();
+  }
+}
+
+// A render object which lays out and paints one subtree while keeping a list
+// of other subtrees alive but not laid out or painted (the "zombie" children).
+//
+// The subtree that is laid out and painted must be a [RenderStack].
+//
+// This class uses [StackParentData] objects for its parent data so that the
+// children of its primary subtree's stack can be moved to this object's list
+// of zombie children without changing their parent data objects.
+class _RenderTheatre extends RenderBox
+    with
+        RenderObjectWithChildMixin<RenderStack>,
+        RenderProxyBoxMixin<RenderStack>,
+        ContainerRenderObjectMixin<RenderBox, StackParentData> {
+  @override
+  void setupParentData(RenderObject child) {
+    if (child.parentData is! StackParentData)
+      child.parentData = StackParentData();
+  }
+
+  // Because both RenderObjectWithChildMixin and ContainerRenderObjectMixin
+  // define redepthChildren, visitChildren and debugDescribeChildren and don't
+  // call super, we have to define them again here to make sure the work of both
+  // is done.
+  //
+  // We chose to put ContainerRenderObjectMixin last in the inheritance chain so
+  // that we can call super to hit its more complex definitions of
+  // redepthChildren and visitChildren, and then duplicate the more trivial
+  // definition from RenderObjectWithChildMixin inline in our version here.
+  //
+  // This code duplication is suboptimal.
+  // TODO(ianh): Replace this with a better solution once https://github.com/dart-lang/sdk/issues/27100 is fixed
+  //
+  // For debugDescribeChildren we just roll our own because otherwise the line
+  // drawings won't really work as well.
+
+  @override
+  void redepthChildren() {
+    if (child != null) redepthChild(child!);
+    super.redepthChildren();
+  }
+
+  @override
+  void visitChildren(RenderObjectVisitor visitor) {
+    if (child != null) visitor(child!);
+    super.visitChildren(visitor);
+  }
+
+  @override
+  List<DiagnosticsNode> debugDescribeChildren() {
+    final List<DiagnosticsNode> children = <DiagnosticsNode>[];
+
+    if (child != null) children.add(child!.toDiagnosticsNode(name: 'onstage'));
+
+    if (firstChild != null) {
+      RenderBox child = firstChild!;
+
+      int count = 1;
+      while (true) {
+        children.add(
+          child.toDiagnosticsNode(
+            name: 'offstage $count',
+            style: DiagnosticsTreeStyle.offstage,
+          ),
+        );
+        if (child == lastChild) break;
+        final StackParentData childParentData =
+            child.parentData! as StackParentData;
+        child = childParentData.nextSibling!;
+        count += 1;
+      }
+    } else {
+      children.add(
+        DiagnosticsNode.message(
+          'no offstage children',
+          style: DiagnosticsTreeStyle.offstage,
+        ),
+      );
+    }
+    return children;
+  }
+
+  @override
+  void visitChildrenForSemantics(RenderObjectVisitor visitor) {
+    if (child != null) visitor(child!);
+  }
+}

File diff suppressed because it is too large
+ 1107 - 0
plugins/reorderables/lib/src/widgets/reorderable_flex.dart


+ 63 - 0
plugins/reorderables/lib/src/widgets/reorderable_mixin.dart

@@ -0,0 +1,63 @@
+import 'package:flutter/widgets.dart';
+
+import 'transitions.dart';
+
+mixin ReorderableMixin {
+  @protected
+  Widget makeAppearingWidget(
+    Widget child,
+    AnimationController entranceController,
+    Size? draggingFeedbackSize,
+    Axis direction,
+  ) {
+    if (null == draggingFeedbackSize) {
+      return SizeTransitionWithIntrinsicSize(
+        sizeFactor: entranceController,
+        axis: direction,
+        child: FadeTransition(
+          opacity: entranceController,
+          child: child,
+        ),
+      );
+    } else {
+      var transition = SizeTransition(
+        sizeFactor: entranceController,
+        axis: direction,
+        child: FadeTransition(opacity: entranceController, child: child),
+      );
+
+      BoxConstraints contentSizeConstraints = BoxConstraints.loose(draggingFeedbackSize);
+      return ConstrainedBox(constraints: contentSizeConstraints, child: transition);
+    }
+  }
+
+  @protected
+  Widget makeDisappearingWidget(
+      Widget child,
+      AnimationController ghostController,
+      Size? draggingFeedbackSize,
+      Axis direction,
+      ) {
+    if (null == draggingFeedbackSize) {
+      return SizeTransitionWithIntrinsicSize(
+        sizeFactor: ghostController,
+        axis: direction,
+        child: FadeTransition(
+          opacity: ghostController,
+          child: child,
+        ),
+      );
+    } else {
+      var transition = SizeTransition(
+        sizeFactor: ghostController,
+        axis: direction,
+        child: FadeTransition(opacity: ghostController, child: child),
+      );
+
+      BoxConstraints contentSizeConstraints =
+      BoxConstraints.loose(draggingFeedbackSize);
+      return ConstrainedBox(
+          constraints: contentSizeConstraints, child: transition);
+    }
+  }
+}

File diff suppressed because it is too large
+ 1015 - 0
plugins/reorderables/lib/src/widgets/reorderable_sliver.dart


+ 287 - 0
plugins/reorderables/lib/src/widgets/reorderable_table.dart

@@ -0,0 +1,287 @@
+//import 'dart:math' as math;
+import 'package:flutter/material.dart';
+
+import './reorderable_flex.dart';
+import './tabluar_flex.dart';
+import './typedefs.dart';
+import '../rendering/tabluar_flex.dart';
+
+class ReorderableTableRow extends TabluarRow {
+  ReorderableTableRow({
+    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
+    MainAxisSize mainAxisSize = MainAxisSize.max,
+    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
+    TextDirection? textDirection,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    TextBaseline? textBaseline,
+    List<Widget> children = const <Widget>[],
+    Decoration? decoration,
+    Key? key,
+  }) : super(
+          children: children,
+          mainAxisAlignment: mainAxisAlignment,
+          mainAxisSize: mainAxisSize,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+          decoration: decoration,
+          key: key,
+        );
+}
+
+typedef DecorateDraggableFeedback = Widget Function(
+    BuildContext feedbackContext, Widget draggableFeedback);
+
+/// Reorderable (drag and drop) version of [Table], a widget that displays its
+/// children in a two-dimensional grid.
+///
+/// The difference between table and list is that cells in a table are
+/// horizontally aligned, whereas in a list, each item can have children but
+/// they are not aligned with children in another item.
+/// Making a row draggable requires cells to be contained in a single widget.
+/// This isn't achievable with [Table] or [GridView] widget since their
+/// children are laid out as cells of widget instead of rows of widget.
+///
+/// Cells of each row must be children of [ReorderableTableRow] and each
+/// [ReorderableTableRow] must have a key.
+///
+/// See also:
+///
+///  * [ReorderableTableRow], which is the container of a row of cells.
+///  * [Table], which uses the table layout algorithm for its children.
+class ReorderableTable extends StatelessWidget {
+  /// Creates a reorderable table.
+  ///
+  /// The [children], [defaultColumnWidth], and [defaultVerticalAlignment]
+  /// arguments must not be null.
+  ReorderableTable({
+    required this.onReorder,
+    this.children = const <ReorderableTableRow>[],
+    this.columnWidths,
+    this.defaultColumnWidth = const FlexColumnWidth(1.0),
+    this.textDirection,
+    this.border,
+    this.defaultVerticalAlignment = TableCellVerticalAlignment.top,
+    this.textBaseline,
+    this.header,
+    this.footer,
+    this.decorateDraggableFeedback,
+    this.onNoReorder,
+    this.reorderAnimationDuration,
+    this.scrollAnimationDuration,
+    this.ignorePrimaryScrollController = false,
+    this.needsLongPressDraggable = true,
+    Key? key,
+    this.borderColor,
+  })  : assert(() {
+          if (children.any((ReorderableTableRow row1) =>
+              row1.key != null &&
+              children.any((ReorderableTableRow row2) =>
+                  row1 != row2 && row1.key == row2.key))) {
+            throw FlutterError(
+                'Two or more ReorderableTableRow children of this Table had the same key.\n'
+                'All the keyed ReorderableTableRow children of a Table must have different Keys.');
+          }
+          return true;
+        }()),
+//      assert(() {
+//        if (children.isNotEmpty) {
+//          final int cellCount = children.first.children.length;
+//          if (children.any((ReorderableTableRow row) => row.children.length != cellCount)) {
+//            throw FlutterError(
+//              'Table contains irregular row lengths.\n'
+//                'Every ReorderableTableRow in a Table must have the same number of children, so that every cell is filled. '
+//                'Otherwise, the table will contain holes.'
+//            );
+//          }
+//        }
+//        return true;
+//      }()),
+
+        super(key: key) {
+    assert(() {
+      final List<Widget> flatChildren = children
+          .expand<Widget>((ReorderableTableRow row) => row.children)
+          .toList(growable: false);
+      if (debugChildrenHaveDuplicateKeys(this, flatChildren)) {
+        throw FlutterError(
+            'Two or more cells in this Table contain widgets with the same key.\n'
+            'Every widget child of every TableRow in a Table must have different keys. The cells of a Table are '
+            'flattened out for processing, so separate cells cannot have duplicate keys even if they are in '
+            'different rows.');
+      }
+      return true;
+    }());
+  }
+
+  /// The rows of the table.
+  ///
+  /// Every row in a table must have the same number of children, and all the
+  /// children must be non-null.
+  final List<ReorderableTableRow> children;
+
+  /// How the horizontal extents of the columns of this table should be determined.
+  ///
+  /// If the [Map] has a null entry for a given column, the table uses the
+  /// [defaultColumnWidth] instead. By default, that uses flex sizing to
+  /// distribute free space equally among the columns.
+  ///
+  /// The [FixedColumnWidth] class can be used to specify a specific width in
+  /// pixels. That is the cheapest way to size a table's columns.
+  ///
+  /// The layout performance of the table depends critically on which column
+  /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is
+  /// quite expensive because it needs to measure each cell in the column to
+  /// determine the intrinsic size of the column.
+  final Map<int, TableColumnWidth>? columnWidths;
+
+  /// How to determine with widths of columns that don't have an explicit sizing algorithm.
+  ///
+  /// Specifically, the [defaultColumnWidth] is used for column `i` if
+  /// `columnWidths[i]` is null.
+  final TableColumnWidth defaultColumnWidth;
+
+  /// The direction in which the columns are ordered.
+  ///
+  /// Defaults to the ambient [Directionality].
+  final TextDirection? textDirection;
+
+  /// The style to use when painting the boundary and interior divisions of the table.
+  final TableBorder? border;
+
+  final Color? borderColor;
+
+  /// How cells that do not explicitly specify a vertical alignment are aligned vertically.
+  final TableCellVerticalAlignment defaultVerticalAlignment;
+
+  /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline].
+  final TextBaseline? textBaseline;
+
+  /// Non-reorderable widget at top of the table. Cells in [header] also affects
+  /// alignment of columns.
+  final Widget? header;
+
+  /// Non-reorderable widget at top of the table. Cells in [footer] also affects
+  /// alignment of columns.
+  final Widget? footer;
+
+  /// Called when a child is dropped into a new position to shuffle the
+  /// children.
+  final ReorderCallback onReorder;
+  final NoReorderCallback? onNoReorder;
+  final DecorateDraggableFeedback? decorateDraggableFeedback;
+  final Duration? reorderAnimationDuration;
+  final Duration? scrollAnimationDuration;
+  final bool ignorePrimaryScrollController;
+
+  final bool needsLongPressDraggable;
+
+  @override
+  Widget build(BuildContext context) {
+//    return TabluarColumn(
+//      mainAxisSize: MainAxisSize.min,
+//      children: itemRows +
+//        [
+//          ReorderableTableRow(key: ValueKey<int>(1), mainAxisSize:MainAxisSize.min, children: <Widget>[Text('111111111111'), Text('222')]),
+//          ReorderableTableRow(key: ValueKey<int>(2), mainAxisSize:MainAxisSize.min, children: <Widget>[Text('33'), Text('4444444444')])
+//        ],
+//    )
+    final GlobalKey tableKey =
+        GlobalKey(debugLabel: '$ReorderableTable table key');
+
+    return ReorderableFlex(
+        header: header,
+        footer: footer,
+        children: children,
+        onReorder: onReorder,
+        onNoReorder: onNoReorder,
+        needsLongPressDraggable: needsLongPressDraggable,
+        direction: Axis.vertical,
+        buildItemsContainer: (BuildContext containerContext, Axis direction,
+            List<Widget> children) {
+          List<Widget> mapped = borderColor == null
+              ? children
+              : children.map<Widget>(
+                  (child) {
+                    final index = children.indexOf(child);
+                    if (index > 0) {
+                      return Container(
+                          decoration: BoxDecoration(
+                            border: Border.symmetric(
+                              horizontal: BorderSide(
+                                  color: borderColor!,
+                                  width:
+                                      index != children.length - 1 ? 0.5 : 1),
+                              vertical: BorderSide(color: borderColor!),
+                            ),
+                          ),
+                          child: child);
+                    }
+                    return child;
+                  },
+                ).toList();
+          return TabluarFlex(
+              key: tableKey,
+              direction: direction,
+//          mainAxisAlignment: mainAxisAlignment,
+//          mainAxisSize: MainAxisSize.min,
+//          crossAxisAlignment: crossAxisAlignment,
+              textDirection: textDirection,
+//          verticalDirection: verticalDirection,
+              textBaseline: textBaseline,
+              children: mapped);
+        },
+        buildDraggableFeedback: (BuildContext feedbackContext,
+            BoxConstraints constraints, Widget child) {
+          // The child is a ReorderableTableRow because children is a List<ReorderableTableRow>
+          ReorderableTableRow tableRow = child as ReorderableTableRow;
+          RenderTabluarFlex renderTabluarFlex =
+              tableKey.currentContext!.findRenderObject() as RenderTabluarFlex;
+          int grandChildIndex = 0;
+          for (;
+              grandChildIndex < tableRow.children.length;
+              grandChildIndex++) {
+            tableRow.children[grandChildIndex] = ConstrainedBox(
+                constraints: BoxConstraints(
+                    minWidth: renderTabluarFlex
+                        .maxGrandchildrenCrossSize[grandChildIndex]!),
+                child: tableRow.children[grandChildIndex]);
+          }
+          for (;
+              grandChildIndex <
+                  renderTabluarFlex.maxGrandchildrenCrossSize.length;
+              grandChildIndex++) {
+            tableRow.children.add(ConstrainedBox(
+              constraints: BoxConstraints(
+                  minWidth: renderTabluarFlex
+                      .maxGrandchildrenCrossSize[grandChildIndex]!),
+            ));
+          }
+
+          ConstrainedBox constrainedTableRow =
+              ConstrainedBox(constraints: constraints, child: tableRow);
+
+          return Transform(
+            transform: new Matrix4.rotationZ(0),
+            alignment: FractionalOffset.topLeft,
+            child: Material(
+//            child: Card(child: ConstrainedBox(constraints: constraints, child: tableRow)),
+              child: (decorateDraggableFeedback ??
+                      defaultDecorateDraggableFeedback)(
+                  feedbackContext, constrainedTableRow),
+              elevation: 6.0,
+              color: Colors.transparent,
+              borderRadius: BorderRadius.zero,
+            ),
+          );
+        },
+        reorderAnimationDuration: reorderAnimationDuration,
+        scrollAnimationDuration: scrollAnimationDuration,
+        ignorePrimaryScrollController: ignorePrimaryScrollController);
+  }
+
+  Widget defaultDecorateDraggableFeedback(
+          BuildContext feedbackContext, Widget draggableFeedback) =>
+      Card(child: draggableFeedback);
+}

+ 23 - 0
plugins/reorderables/lib/src/widgets/reorderable_widget.dart

@@ -0,0 +1,23 @@
+import 'package:flutter/widgets.dart';
+
+class ReorderableWidget extends StatelessWidget implements ReorderableItem {
+  final Widget child;
+  final bool reorderable;
+
+  ReorderableWidget({
+    required this.child,
+    required this.reorderable,
+    required Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return child;
+  }
+}
+
+abstract class ReorderableItem extends Widget {
+  final bool reorderable;
+
+  ReorderableItem({required this.reorderable});
+}

File diff suppressed because it is too large
+ 1287 - 0
plugins/reorderables/lib/src/widgets/reorderable_wrap.dart


+ 11 - 0
plugins/reorderables/lib/src/widgets/safe_state.dart

@@ -0,0 +1,11 @@
+import 'package:flutter/widgets.dart';
+
+mixin SafeStateMixin<T extends StatefulWidget> on State<T> {
+  @override
+  void setState(VoidCallback fn) {
+    //can't call setState() if the stateful widget is not mounted, i.e. removed from the tree.
+    if (this.mounted) {
+      super.setState(fn);
+    }
+  }
+}

+ 452 - 0
plugins/reorderables/lib/src/widgets/tabluar_flex.dart

@@ -0,0 +1,452 @@
+import 'package:flutter/widgets.dart';
+
+import '../rendering/tabluar_flex.dart';
+
+class TabluarFlex extends Flex {
+  TabluarFlex({
+    required Axis direction,
+    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
+    MainAxisSize mainAxisSize = MainAxisSize.max,
+    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
+    TextDirection? textDirection,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    TextBaseline? textBaseline,
+    List<Widget> children = const <Widget>[],
+    this.decoration,
+    Key? key,
+  }) : super(
+          key: key,
+          children: children,
+          direction: direction,
+          mainAxisAlignment: mainAxisAlignment,
+          mainAxisSize: mainAxisSize,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+        );
+
+  /// What decoration to paint.
+  ///
+  /// Commonly a [BoxDecoration].
+  final Decoration? decoration;
+
+  @override
+  RenderTabluarFlex createRenderObject(BuildContext context) {
+    return RenderTabluarFlex(
+      direction: direction,
+      mainAxisAlignment: mainAxisAlignment,
+      mainAxisSize: mainAxisSize,
+      crossAxisAlignment: crossAxisAlignment,
+      textDirection: getEffectiveTextDirection(context),
+      verticalDirection: verticalDirection,
+      textBaseline: textBaseline,
+      decoration: decoration,
+      configuration: createLocalImageConfiguration(context),
+    );
+  }
+
+  @override
+  void updateRenderObject(
+      BuildContext context, RenderTabluarFlex renderObject) {
+    renderObject
+      ..direction = direction
+      ..mainAxisAlignment = mainAxisAlignment
+      ..mainAxisSize = mainAxisSize
+      ..crossAxisAlignment = crossAxisAlignment
+      ..textDirection = getEffectiveTextDirection(context)
+      ..verticalDirection = verticalDirection
+      ..textBaseline = textBaseline
+      ..decoration = decoration
+      ..configuration = createLocalImageConfiguration(context);
+  }
+}
+
+/// A widget that displays its children in a horizontal array.
+///
+/// To cause a child to expand to fill the available horizontal space, wrap the
+/// child in an [Expanded] widget.
+///
+/// The [Row] widget does not scroll (and in general it is considered an error
+/// to have more children in a [Row] than will fit in the available room). If
+/// you have a line of widgets and want them to be able to scroll if there is
+/// insufficient room, consider using a [ListView].
+///
+/// For a vertical variant, see [Column].
+///
+/// If you only have one child, then consider using [Align] or [Center] to
+/// position the child.
+///
+/// {@tool sample}
+///
+/// This example divides the available space into three (horizontally), and
+/// places text centered in the first two cells and the Flutter logo centered in
+/// the third:
+///
+/// ```dart
+/// Row(
+///   children: <Widget>[
+///     Expanded(
+///       child: Text('Deliver features faster', textAlign: TextAlign.center),
+///     ),
+///     Expanded(
+///       child: Text('Craft beautiful UIs', textAlign: TextAlign.center),
+///     ),
+///     Expanded(
+///       child: FittedBox(
+///         fit: BoxFit.contain, // otherwise the logo will be tiny
+///         child: const FlutterLogo(),
+///       ),
+///     ),
+///   ],
+/// )
+/// ```
+/// {@end-tool}
+///
+/// ## Troubleshooting
+///
+/// ### Why does my row have a yellow and black warning stripe?
+///
+/// If the non-flexible contents of the row (those that are not wrapped in
+/// [Expanded] or [Flexible] widgets) are together wider than the row itself,
+/// then the row is said to have overflowed. When a row overflows, the row does
+/// not have any remaining space to share between its [Expanded] and [Flexible]
+/// children. The row reports this by drawing a yellow and black striped
+/// warning box on the edge that is overflowing. If there is room on the outside
+/// of the row, the amount of overflow is printed in red lettering.
+///
+/// {@tool sample}
+///
+/// #### Story time
+///
+/// Suppose, for instance, that you had this code:
+///
+/// ```dart
+/// Row(
+///   children: <Widget>[
+///     const FlutterLogo(),
+///     const Text('Flutter\'s hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android.'),
+///     const Icon(Icons.sentiment_very_satisfied),
+///   ],
+/// )
+/// ```
+/// {@end-tool}
+///
+/// The row first asks its first child, the [FlutterLogo], to lay out, at
+/// whatever size the logo would like. The logo is friendly and happily decides
+/// to be 24 pixels to a side. This leaves lots of room for the next child. The
+/// row then asks that next child, the text, to lay out, at whatever size it
+/// thinks is best.
+///
+/// At this point, the text, not knowing how wide is too wide, says "Ok, I will
+/// be thiiiiiiiiiiiiiiiiiiiis wide.", and goes well beyond the space that the
+/// row has available, not wrapping. The row responds, "That's not fair, now I
+/// have no more room available for my other children!", and gets angry and
+/// sprouts a yellow and black strip.
+///
+/// {@tool sample}
+/// The fix is to wrap the second child in an [Expanded] widget, which tells the
+/// row that the child should be given the remaining room:
+///
+/// ```dart
+/// Row(
+///   children: <Widget>[
+///     const FlutterLogo(),
+///     const Expanded(
+///       child: Text('Flutter\'s hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android.'),
+///     ),
+///     const Icon(Icons.sentiment_very_satisfied),
+///   ],
+/// )
+/// ```
+/// {@end-tool}
+///
+/// Now, the row first asks the logo to lay out, and then asks the _icon_ to lay
+/// out. The [Icon], like the logo, is happy to take on a reasonable size (also
+/// 24 pixels, not coincidentally, since both [FlutterLogo] and [Icon] honor the
+/// ambient [IconTheme]). This leaves some room left over, and now the row tells
+/// the text exactly how wide to be: the exact width of the remaining space. The
+/// text, now happy to comply to a reasonable request, wraps the text within
+/// that width, and you end up with a paragraph split over several lines.
+///
+/// ## Layout algorithm
+///
+/// _This section describes how a [Row] is rendered by the framework._
+/// _See [BoxConstraints] for an introduction to box layout models._
+///
+/// Layout for a [Row] proceeds in six steps:
+///
+/// 1. Layout each child a null or zero flex factor (e.g., those that are not
+///    [Expanded]) with unbounded horizontal constraints and the incoming
+///    vertical constraints. If the [crossAxisAlignment] is
+///    [CrossAxisAlignment.stretch], instead use tight vertical constraints that
+///    match the incoming max height.
+/// 2. Divide the remaining horizontal space among the children with non-zero
+///    flex factors (e.g., those that are [Expanded]) according to their flex
+///    factor. For example, a child with a flex factor of 2.0 will receive twice
+///    the amount of horizontal space as a child with a flex factor of 1.0.
+/// 3. Layout each of the remaining children with the same vertical constraints
+///    as in step 1, but instead of using unbounded horizontal constraints, use
+///    horizontal constraints based on the amount of space allocated in step 2.
+///    Children with [Flexible.fit] properties that are [FlexFit.tight] are
+///    given tight constraints (i.e., forced to fill the allocated space), and
+///    children with [Flexible.fit] properties that are [FlexFit.loose] are
+///    given loose constraints (i.e., not forced to fill the allocated space).
+/// 4. The height of the [Row] is the maximum height of the children (which will
+///    always satisfy the incoming vertical constraints).
+/// 5. The width of the [Row] is determined by the [mainAxisSize] property. If
+///    the [mainAxisSize] property is [MainAxisSize.max], then the width of the
+///    [Row] is the max width of the incoming constraints. If the [mainAxisSize]
+///    property is [MainAxisSize.min], then the width of the [Row] is the sum
+///    of widths of the children (subject to the incoming constraints).
+/// 6. Determine the position for each child according to the
+///    [mainAxisAlignment] and the [crossAxisAlignment]. For example, if the
+///    [mainAxisAlignment] is [MainAxisAlignment.spaceBetween], any horizontal
+///    space that has not been allocated to children is divided evenly and
+///    placed between the children.
+///
+/// See also:
+///
+///  * [Column], for a vertical equivalent.
+///  * [Flex], if you don't know in advance if you want a horizontal or vertical
+///    arrangement.
+///  * [Expanded], to indicate children that should take all the remaining room.
+///  * [Flexible], to indicate children that should share the remaining room but
+///    that may by sized smaller (leaving some remaining room unused).
+///  * [Spacer], a widget that takes up space proportional to it's flex value.
+///  * The [catalog of layout widgets](https://flutter.io/widgets/layout/).
+class TabluarRow extends TabluarFlex {
+  /// Creates a horizontal array of children.
+  ///
+  /// The [direction], [mainAxisAlignment], [mainAxisSize],
+  /// [crossAxisAlignment], and [verticalDirection] arguments must not be null.
+  /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then
+  /// [textBaseline] must not be null.
+  ///
+  /// The [textDirection] argument defaults to the ambient [Directionality], if
+  /// any. If there is no ambient directionality, and a text direction is going
+  /// to be necessary to determine the layout order (which is always the case
+  /// unless the row has no children or only one child) or to disambiguate
+  /// `start` or `end` values for the [mainAxisAlignment], the [textDirection]
+  /// must not be null.
+  TabluarRow({
+    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
+    MainAxisSize mainAxisSize = MainAxisSize.max,
+    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
+    TextDirection? textDirection,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    TextBaseline? textBaseline,
+    List<Widget> children = const <Widget>[],
+    Decoration? decoration,
+    Key? key,
+  }) : super(
+          children: children,
+          key: key,
+          direction: Axis.horizontal,
+          mainAxisAlignment: mainAxisAlignment,
+          mainAxisSize: mainAxisSize,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+          decoration: decoration,
+        );
+}
+
+/// A widget that displays its children in a vertical array.
+///
+/// To cause a child to expand to fill the available vertical space, wrap the
+/// child in an [Expanded] widget.
+///
+/// The [Column] widget does not scroll (and in general it is considered an error
+/// to have more children in a [Column] than will fit in the available room). If
+/// you have a line of widgets and want them to be able to scroll if there is
+/// insufficient room, consider using a [ListView].
+///
+/// For a horizontal variant, see [Row].
+///
+/// If you only have one child, then consider using [Align] or [Center] to
+/// position the child.
+///
+/// {@tool sample}
+///
+/// This example uses a [Column] to arrange three widgets vertically, the last
+/// being made to fill all the remaining space.
+///
+/// ```dart
+/// Column(
+///   children: <Widget>[
+///     Text('Deliver features faster'),
+///     Text('Craft beautiful UIs'),
+///     Expanded(
+///       child: FittedBox(
+///         fit: BoxFit.contain, // otherwise the logo will be tiny
+///         child: const FlutterLogo(),
+///       ),
+///     ),
+///   ],
+/// )
+/// ```
+/// {@end-tool}
+/// {@tool sample}
+///
+/// In the sample above, the text and the logo are centered on each line. In the
+/// following example, the [crossAxisAlignment] is set to
+/// [CrossAxisAlignment.start], so that the children are left-aligned. The
+/// [mainAxisSize] is set to [MainAxisSize.min], so that the column shrinks to
+/// fit the children.
+///
+/// ```dart
+/// Column(
+///   crossAxisAlignment: CrossAxisAlignment.start,
+///   mainAxisSize: MainAxisSize.min,
+///   children: <Widget>[
+///     Text('We move under cover and we move as one'),
+///     Text('Through the night, we have one shot to live another day'),
+///     Text('We cannot let a stray gunshot give us away'),
+///     Text('We will fight up close, seize the moment and stay in it'),
+///     Text('It’s either that or meet the business end of a bayonet'),
+///     Text('The code word is ‘Rochambeau,’ dig me?'),
+///     Text('Rochambeau!', style: DefaultTextStyle.of(context).style.apply(fontSizeFactor: 2.0)),
+///   ],
+/// )
+/// ```
+/// {@end-tool}
+///
+/// ## Troubleshooting
+///
+/// ### When the incoming vertical constraints are unbounded
+///
+/// When a [Column] has one or more [Expanded] or [Flexible] children, and is
+/// placed in another [Column], or in a [ListView], or in some other context
+/// that does not provide a maximum height constraint for the [Column], you will
+/// get an exception at runtime saying that there are children with non-zero
+/// flex but the vertical constraints are unbounded.
+///
+/// The problem, as described in the details that accompany that exception, is
+/// that using [Flexible] or [Expanded] means that the remaining space after
+/// laying out all the other children must be shared equally, but if the
+/// incoming vertical constraints are unbounded, there is infinite remaining
+/// space.
+///
+/// The key to solving this problem is usually to determine why the [Column] is
+/// receiving unbounded vertical constraints.
+///
+/// One common reason for this to happen is that the [Column] has been placed in
+/// another [Column] (without using [Expanded] or [Flexible] around the inner
+/// nested [Column]). When a [Column] lays out its non-flex children (those that
+/// have neither [Expanded] or [Flexible] around them), it gives them unbounded
+/// constraints so that they can determine their own dimensions (passing
+/// unbounded constraints usually signals to the child that it should
+/// shrink-wrap its contents). The solution in this case is typically to just
+/// wrap the inner column in an [Expanded] to indicate that it should take the
+/// remaining space of the outer column, rather than being allowed to take any
+/// amount of room it desires.
+///
+/// Another reason for this message to be displayed is nesting a [Column] inside
+/// a [ListView] or other vertical scrollable. In that scenario, there really is
+/// infinite vertical space (the whole point of a vertical scrolling list is to
+/// allow infinite space vertically). In such scenarios, it is usually worth
+/// examining why the inner [Column] should have an [Expanded] or [Flexible]
+/// child: what size should the inner children really be? The solution in this
+/// case is typically to remove the [Expanded] or [Flexible] widgets from around
+/// the inner children.
+///
+/// For more discussion about constraints, see [BoxConstraints].
+///
+/// ### The yellow and black striped banner
+///
+/// When the contents of a [Column] exceed the amount of space available, the
+/// [Column] overflows, and the contents are clipped. In debug mode, a yellow
+/// and black striped bar is rendered at the overflowing edge to indicate the
+/// problem, and a message is printed below the [Column] saying how much
+/// overflow was detected.
+///
+/// The usual solution is to use a [ListView] rather than a [Column], to enable
+/// the contents to scroll when vertical space is limited.
+///
+/// ## Layout algorithm
+///
+/// _This section describes how a [Column] is rendered by the framework._
+/// _See [BoxConstraints] for an introduction to box layout models._
+///
+/// Layout for a [Column] proceeds in six steps:
+///
+/// 1. Layout each child a null or zero flex factor (e.g., those that are not
+///    [Expanded]) with unbounded vertical constraints and the incoming
+///    horizontal constraints. If the [crossAxisAlignment] is
+///    [CrossAxisAlignment.stretch], instead use tight horizontal constraints
+///    that match the incoming max width.
+/// 2. Divide the remaining vertical space among the children with non-zero
+///    flex factors (e.g., those that are [Expanded]) according to their flex
+///    factor. For example, a child with a flex factor of 2.0 will receive twice
+///    the amount of vertical space as a child with a flex factor of 1.0.
+/// 3. Layout each of the remaining children with the same horizontal
+///    constraints as in step 1, but instead of using unbounded vertical
+///    constraints, use vertical constraints based on the amount of space
+///    allocated in step 2. Children with [Flexible.fit] properties that are
+///    [FlexFit.tight] are given tight constraints (i.e., forced to fill the
+///    allocated space), and children with [Flexible.fit] properties that are
+///    [FlexFit.loose] are given loose constraints (i.e., not forced to fill the
+///    allocated space).
+/// 4. The width of the [Column] is the maximum width of the children (which
+///    will always satisfy the incoming horizontal constraints).
+/// 5. The height of the [Column] is determined by the [mainAxisSize] property.
+///    If the [mainAxisSize] property is [MainAxisSize.max], then the height of
+///    the [Column] is the max height of the incoming constraints. If the
+///    [mainAxisSize] property is [MainAxisSize.min], then the height of the
+///    [Column] is the sum of heights of the children (subject to the incoming
+///    constraints).
+/// 6. Determine the position for each child according to the
+///    [mainAxisAlignment] and the [crossAxisAlignment]. For example, if the
+///    [mainAxisAlignment] is [MainAxisAlignment.spaceBetween], any vertical
+///    space that has not been allocated to children is divided evenly and
+///    placed between the children.
+///
+/// See also:
+///
+///  * [Row], for a horizontal equivalent.
+///  * [Flex], if you don't know in advance if you want a horizontal or vertical
+///    arrangement.
+///  * [Expanded], to indicate children that should take all the remaining room.
+///  * [Flexible], to indicate children that should share the remaining room but
+///    that may size smaller (leaving some remaining room unused).
+///  * [SingleChildScrollView], whose documentation discusses some ways to
+///    use a [Column] inside a scrolling container.
+///  * [Spacer], a widget that takes up space proportional to it's flex value.
+///  * The [catalog of layout widgets](https://flutter.io/widgets/layout/).
+class TabluarColumn extends TabluarFlex {
+  /// Creates a vertical array of children.
+  ///
+  /// The [direction], [mainAxisAlignment], [mainAxisSize],
+  /// [crossAxisAlignment], and [verticalDirection] arguments must not be null.
+  /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then
+  /// [textBaseline] must not be null.
+  ///
+  /// The [textDirection] argument defaults to the ambient [Directionality], if
+  /// any. If there is no ambient directionality, and a text direction is going
+  /// to be necessary to disambiguate `start` or `end` values for the
+  /// [crossAxisAlignment], the [textDirection] must not be null.
+  TabluarColumn({
+    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
+    MainAxisSize mainAxisSize = MainAxisSize.max,
+    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    List<Widget> children = const <Widget>[],
+    TextDirection? textDirection,
+    TextBaseline? textBaseline,
+    Decoration? decoration,
+    Key? key,
+  }) : super(
+          children: children,
+          key: key,
+          direction: Axis.vertical,
+          mainAxisAlignment: mainAxisAlignment,
+          mainAxisSize: mainAxisSize,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+          decoration: decoration,
+        );
+}

+ 55 - 0
plugins/reorderables/lib/src/widgets/transitions.dart

@@ -0,0 +1,55 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter/foundation.dart';
+
+import '../rendering/transitions.dart';
+
+class SizeTransitionWithIntrinsicSize extends SingleChildRenderObjectWidget {
+  /// Creates a size transition with its intrinsic width/height taking [sizeFactor] into account.
+  ///
+  /// The [axis], [sizeFactor], and [axisAlignment] arguments must not be null.
+  /// The [axis] argument defaults to [Axis.vertical]. The [axisAlignment]
+  /// defaults to 0.0, which centers the child along the main axis during the
+  /// transition.
+  SizeTransitionWithIntrinsicSize({
+    this.axis = Axis.vertical,
+    required this.sizeFactor,
+    double axisAlignment = 0.0,
+    Widget? child,
+    Key? key,
+  }) : super(
+            key: key,
+            child: SizeTransition(
+              axis: axis,
+              sizeFactor: sizeFactor,
+              axisAlignment: axisAlignment,
+              child: child,
+            ));
+
+  final Axis axis;
+  final Animation<double> sizeFactor;
+
+  @override
+  RenderSizeTransitionWithIntrinsicSize createRenderObject(
+      BuildContext context) {
+    return RenderSizeTransitionWithIntrinsicSize(
+      axis: axis,
+      sizeFactor: sizeFactor,
+    );
+  }
+
+  @override
+  void updateRenderObject(BuildContext context,
+      RenderSizeTransitionWithIntrinsicSize renderObject) {
+    renderObject
+      ..axis = axis
+      ..sizeFactor = sizeFactor;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<Axis>('axis', axis));
+    properties
+        .add(DiagnosticsProperty<Animation<double>>('sizeFactor', sizeFactor));
+  }
+}

+ 9 - 0
plugins/reorderables/lib/src/widgets/typedefs.dart

@@ -0,0 +1,9 @@
+import 'package:flutter/widgets.dart';
+
+typedef BuildItemsContainer = Widget Function(
+    BuildContext context, Axis direction, List<Widget> children);
+typedef BuildDraggableFeedback = Widget Function(
+    BuildContext context, BoxConstraints constraints, Widget child);
+
+typedef NoReorderCallback = void Function(int index);
+typedef ReorderStartedCallback = void Function(int index);

+ 57 - 0
plugins/reorderables/lib/src/widgets/wrap.dart

@@ -0,0 +1,57 @@
+import 'package:flutter/widgets.dart';
+
+import '../rendering/wrap.dart';
+
+class WrapWithMainAxisCount extends Wrap {
+  WrapWithMainAxisCount({
+    Key? key,
+    Axis direction = Axis.horizontal,
+    WrapAlignment alignment = WrapAlignment.start,
+    double spacing = 0.0,
+    WrapAlignment runAlignment = WrapAlignment.start,
+    double runSpacing = 0.0,
+    WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start,
+    TextDirection? textDirection,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    List<Widget> children = const <Widget>[],
+    this.minMainAxisCount,
+    this.maxMainAxisCount,
+  }) : super(
+            key: key,
+            direction: direction,
+            alignment: alignment,
+            spacing: spacing,
+            runAlignment: runAlignment,
+            runSpacing: runSpacing,
+            crossAxisAlignment: crossAxisAlignment,
+            textDirection: textDirection,
+            verticalDirection: verticalDirection,
+            children: children);
+
+  final int? minMainAxisCount;
+  final int? maxMainAxisCount;
+
+  @override
+  RenderWrapWithMainAxisCount createRenderObject(BuildContext context) {
+    return RenderWrapWithMainAxisCount(
+        direction: direction,
+        alignment: alignment,
+        spacing: spacing,
+        runAlignment: runAlignment,
+        runSpacing: runSpacing,
+        crossAxisAlignment: crossAxisAlignment,
+        textDirection: textDirection ?? Directionality.of(context),
+        verticalDirection: verticalDirection,
+        minMainAxisCount: minMainAxisCount,
+        maxMainAxisCount: maxMainAxisCount);
+  }
+
+  @override
+  void updateRenderObject(
+      BuildContext context, RenderWrapWithMainAxisCount renderObject) {
+    super.updateRenderObject(context, renderObject);
+    renderObject
+      ..minMainAxisCount = minMainAxisCount
+      ..maxMainAxisCount = maxMainAxisCount;
+  }
+}

+ 53 - 0
plugins/reorderables/pubspec.yaml

@@ -0,0 +1,53 @@
+name: reorderables
+description: Reorderable table, row, column, wrap, sliver list that allow drag and drop of their children.
+version: 0.6.0
+homepage: https://github.com/hanshengchiu/reorderables
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://www.dartlang.org/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+
+  # To add assets to your package, add an assets section, like this:
+  # assets:
+  #  - images/a_dot_burr.jpeg
+  #  - images/a_dot_ham.jpeg
+  #
+  # For details regarding assets in packages, see
+  # https://flutter.io/assets-and-images/#from-packages
+  #
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.io/assets-and-images/#resolution-aware.
+
+  # To add custom fonts to your package, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts in packages, see
+  # https://flutter.io/custom-fonts/#from-packages

+ 0 - 0
plugins/reorderables/res/values/strings_en.arb


+ 13 - 0
plugins/reorderables/test/reorderables_test.dart

@@ -0,0 +1,13 @@
+import 'package:flutter_test/flutter_test.dart';
+
+//import 'package:reorderables/reorderables.dart';
+
+void main() {
+  test('adds one to input values', () {
+//    final calculator = Calculator();
+//    expect(calculator.addOne(2), 3);
+//    expect(calculator.addOne(-7), -6);
+//    expect(calculator.addOne(0), 1);
+//    expect(() => calculator.addOne(null), throwsNoSuchMethodError);
+  });
+}

+ 16 - 1
pubspec.lock

@@ -218,7 +218,7 @@ packages:
     source: hosted
     version: "4.10.1"
   collection:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: collection
       sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
@@ -321,6 +321,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
+  dotted_border:
+    dependency: "direct main"
+    description:
+      name: dotted_border
+      sha256: "108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   easy_refresh:
     dependency: "direct main"
     description:
@@ -996,6 +1004,13 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.1.0"
+  reorderables:
+    dependency: "direct main"
+    description:
+      path: "plugins/reorderables"
+      relative: true
+    source: path
+    version: "0.6.0"
   retrofit:
     dependency: "direct main"
     description:

+ 9 - 0
pubspec.yaml

@@ -52,6 +52,15 @@ dependencies:
   #上、下拉刷新
   easy_refresh: ^3.4.0
 
+  #列表拖动
+  reorderables:
+    path: plugins/reorderables
+
+  collection: ^1.19.1
+
+  #虚线
+  dotted_border: ^2.1.0
+
   #网络图片
   cached_network_image: ^3.4.1