Pārlūkot izejas kodu

Merge branch 'a'

zhaoyadi 3 gadi atpakaļ
vecāks
revīzija
2cc9a92b52

+ 6 - 0
.run/main.run.xml

@@ -0,0 +1,6 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="main" type="FlutterRunConfigurationType" factoryName="Flutter">
+    <option name="filePath" value="$PROJECT_DIR$/example/lib/main.dart" />
+    <method v="2" />
+  </configuration>
+</component>

+ 0 - 3
CHANGELOG.md

@@ -1,3 +0,0 @@
-## 0.0.1
-
-* TODO: Describe initial release.

+ 0 - 1
LICENSE

@@ -1 +0,0 @@
-TODO: Add your license here.

+ 0 - 39
README.md

@@ -1,39 +0,0 @@
-<!-- 
-This README describes the package. If you publish this package to pub.dev,
-this README's contents appear on the landing page for your package.
-
-For information about how to write a good package README, see the guide for
-[writing package pages](https://dart.dev/guides/libraries/writing-package-pages). 
-
-For general information about developing packages, see the Dart guide for
-[creating packages](https://dart.dev/guides/libraries/create-library-packages)
-and the Flutter guide for
-[developing packages and plugins](https://flutter.dev/developing-packages). 
--->
-
-TODO: Put a short description of the package here that helps potential users
-know whether this package might be useful for them.
-
-## Features
-
-TODO: List what your package can do. Maybe include images, gifs, or videos.
-
-## Getting started
-
-TODO: List prerequisites and provide or point to information on how to
-start using the package.
-
-## Usage
-
-TODO: Include short and useful examples for package users. Add longer examples
-to `/example` folder. 
-
-```dart
-const like = 'sample';
-```
-
-## Additional information
-
-TODO: Tell users more about the package: where to find more information, how to 
-contribute to the package, how to file issues, what response they can expect 
-from the package authors, and more.

+ 0 - 3
example/android/app/build.gradle

@@ -42,7 +42,6 @@ android {
     }
 
     defaultConfig {
-        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId "com.example.example"
         minSdkVersion 16
         targetSdkVersion 30
@@ -52,8 +51,6 @@ android {
 
     buildTypes {
         release {
-            // TODO: Add your own signing config for the release build.
-            // Signing with the debug keys for now, so `flutter run --release` works.
             signingConfig signingConfigs.debug
         }
     }

+ 182 - 0
example/lib/demo/demo1.dart

@@ -0,0 +1,182 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:luojigou_thinking_core/luojigou_thinking_core.dart';
+
+class Demo1 extends StatefulWidget {
+  const Demo1({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _Demo1State();
+}
+
+class _Demo1State extends State<Demo1> with SingleTickerProviderStateMixin {
+  late TabController _controller;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TabController(length: 7, vsync: this);
+  }
+
+  bool _value = false;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      backgroundColor: Color(0xFFC8DDFF),
+      body: ReverseRow(
+        children: [
+          PutAway(
+            direction: PutAwayDirection.rtl,
+            isPutAway: _value,
+            child: SizedBox(
+              width: 55,
+              height: double.infinity,
+              child: ReverseColumn(
+                children: [
+                  SizedBox(
+                    width: double.infinity,
+                    height: 44 + MediaQuery.of(context).padding.top,
+                    child: const Icon(Icons.arrow_back_ios),
+                  ),
+                  Expanded(
+                    child: VerticalTabBar(
+                      controller: _controller,
+                      isScrollable: true,
+                      padding: EdgeInsets.only(left: 40),
+                      labelStyle: const TextStyle(
+                        color: Color(0xFF75AAFF),
+                        fontSize: 16,
+                        height: 1,
+                      ),
+                      unselectedLabelStyle: const TextStyle(
+                        color: Color(0xFFFFFFFF),
+                        fontSize: 16,
+                        height: 1,
+                      ),
+                      labelColor: const Color(0xFF75AAFF),
+                      unselectedLabelColor: Colors.white,
+                      indicator: _TabDecoration(),
+                      tabs: [
+                        Container(
+                          padding: EdgeInsets.symmetric(horizontal: 4),
+                          height: double.infinity,
+                          color: Colors.transparent,
+                          child: Center(child: const Text('综合区')),
+                        ),
+                        Container(
+                          padding: EdgeInsets.symmetric(horizontal: 4),
+                          height: double.infinity,
+                          color: Colors.transparent,
+                          child: Center(child: const Text('中华文化')),
+                        ),
+                        Container(
+                          padding: EdgeInsets.symmetric(horizontal: 4),
+                          height: double.infinity,
+                          color: Colors.transparent,
+                          child: Center(child: const Text('科学探索')),
+                        ),
+                        Container(
+                          padding: EdgeInsets.symmetric(horizontal: 4),
+                          height: double.infinity,
+                          color: Colors.transparent,
+                          child: Center(child: const Text('自由探索')),
+                        ),
+                        Container(
+                          padding: EdgeInsets.symmetric(horizontal: 4),
+                          height: double.infinity,
+                          color: Colors.transparent,
+                          child: Center(child: const Text('思维探索')),
+                        ),
+
+                        Container(
+                          padding: EdgeInsets.symmetric(horizontal: 4),
+                          height: double.infinity,
+                          color: Colors.transparent,
+                          child: Center(child: const Text('思维探索')),
+                        ),
+                        Container(
+                          padding: EdgeInsets.symmetric(horizontal: 4),
+                          height: double.infinity,
+                          color: Colors.transparent,
+                          child: Center(child: const Text('思维探索')),
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          Expanded(
+            child: GestureDetector(
+              onTap: () {
+                setState(() {
+                  _value = !_value;
+                });
+              },
+              child: Container(
+                decoration: const BoxDecoration(
+                  color: Colors.white,
+                  borderRadius: BorderRadius.only(
+                    topLeft: Radius.circular(20),
+                    bottomLeft: Radius.circular(20),
+                  ),
+                  boxShadow: [],
+                ),
+                child: Center(
+                  child: Container(
+                    height: 220,
+                    width: 220,
+                    color: Colors.red,
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _TabDecoration extends Decoration {
+  @override
+  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
+    return _BoxPainter(onChanged);
+  }
+}
+
+class _BoxPainter extends BoxPainter {
+  _BoxPainter(VoidCallback? onChanged) : super(onChanged);
+
+  @override
+  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
+    if (configuration.size == null) return;
+
+    final size = configuration.size!;
+    final paint = Paint()..color = const Color(0xFF75AAFF);
+
+    final dotx = offset.dx + size.width / 2;
+    final doty = offset.dy + size.height - 8;
+    canvas.drawCircle(Offset(dotx, doty), 2, paint);
+
+    double h = 18.8;
+    double w = 60;
+
+    final path = Path();
+    path.moveTo(dotx - w, doty + 8);
+    path.lineTo(dotx, doty + 8 + 20);
+    path.lineTo(dotx + w, doty + 8);
+
+    path.reset();
+    path.moveTo(dotx - w, doty + 8);
+    path.cubicTo(dotx - w / 2 - 10, doty + 8, dotx - w / 2 + 10, doty + 8 + h,
+        dotx, doty + 8 + h);
+    path.cubicTo(dotx + w / 2 - 10, doty + 8 + h, dotx + w / 2 + 10, doty + 8,
+        dotx + w, doty + 8);
+    canvas.drawPath(path, paint..color = const Color(0xFFC8DDFF));
+  }
+}

+ 201 - 0
example/lib/demo/demo2.dart

@@ -0,0 +1,201 @@
+import 'dart:ui';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+class Demo2 extends StatefulWidget {
+  const Demo2({Key? key}) : super(key: key);
+
+  @override
+  State<Demo2> createState() => _Demo2State();
+}
+
+class _Demo2State extends State<Demo2> {
+  late ScrollController _scrollController;
+
+  @override
+  void initState() {
+    super.initState();
+    _scrollController = ScrollController();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        ColoredBox(
+          color: const Color(0xFFC8DDFF),
+          child: SizedBox(
+            width: 55,
+            child: ListView.builder(
+              itemBuilder: (context, index) {
+                return CustomPaint(
+                  painter: index == 2 ? _Painter() : null,
+                  child: SizedBox(
+                    width: double.infinity,
+                    height: 100,
+                    child: Center(child: Text("Hello $index")),
+                  ),
+                );
+              },
+            ),
+          ),
+        ),
+        Expanded(
+          child: Column(
+            children: [
+              Expanded(
+                child: Container(
+                  color: Colors.yellow,
+                ),
+              ),
+              const Expanded(
+                child: TestWidget(
+                  child: TestWidget(),
+                ),
+              ),
+              Expanded(
+                child: Container(
+                  color: Colors.lightGreen,
+                ),
+              ),
+              Expanded(
+                child: Container(
+                  color: Colors.redAccent,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+const double _width = 18.74;
+const double _height = 158.41;
+
+class _Painter extends CustomPainter {
+  @override
+  void paint(Canvas canvas, Size size) {
+    canvas.drawCircle(
+      Offset(size.width - 5 - 2, size.height / 2),
+      2,
+      Paint()..color = Colors.green,
+    );
+
+    var path = Path();
+    path.reset();
+    path.moveTo(size.width, size.height / 2 - 20);
+    path.lineTo(size.width + 20, size.height / 2);
+    path.lineTo(size.width, size.height / 2 + 20);
+    path.close();
+
+    canvas.drawPath(
+      path,
+      Paint()
+        ..color = Colors.blue
+        ..style = PaintingStyle.fill,
+    );
+    // canvas.drawRect(Offset.zero & size, Paint()..color = Colors.green);
+  }
+
+  @override
+  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
+}
+
+class TestWidget extends SingleChildRenderObjectWidget {
+  const TestWidget({
+    Key? key,
+    Widget? child,
+  }) : super(key: key, child: child);
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderTest();
+  }
+}
+
+class _RenderTest extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
+  @override
+  void performLayout() {
+    child?.layout(constraints.deflate(const EdgeInsets.all(20)));
+    size = constraints.constrain(Size.infinite);
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    return constraints.constrain(Size.infinite);
+
+    Listener listener;
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    super.paint(context, offset);
+    Color color;
+    if (parent is _RenderTest) {
+      if (_touchOffset != null) {
+        color = Colors.black;
+      } else {
+        color = Colors.red;
+      }
+    } else {
+      if (_touchOffset != null) {
+        color = Colors.tealAccent;
+      } else {
+        color = Colors.deepPurple;
+      }
+    }
+
+    context.canvas.drawRect(offset & size, Paint()
+      ..color = color);
+    if (child != null) {
+      context.paintChild(child!, offset);
+    }
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+    if(child != null){
+      return child!.hitTest(result, position: position);
+    }
+    return super.hitTestChildren(result, position: position);
+  }
+
+  @override
+  bool hitTestSelf(Offset position) => size.contains(position);
+
+  Offset? _touchOffset;
+
+  GestureRecognizer gestureDetector = TapGestureRecognizer()
+    ..onTap = () {
+      debugPrint("TapGestureRecognizer onTap");
+    }
+    ..onTapDown = (details) {
+      debugPrint("TapGestureRecognizer onTapDown");
+    }
+    ..onTapUp = (details) {
+      debugPrint("TapGestureRecognizer onTapUp");
+    }
+    ..onTapCancel = () {
+      debugPrint("TapGestureRecognizer onTapCancel");
+    };
+
+  @override
+  void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
+    debugPrint(event.runtimeType.toString());
+    debugPrint(event.position.toString());
+    if (event is PointerDownEvent) {
+      _touchOffset = event.localPosition;
+      gestureDetector.addPointer(event);
+    } else if (event is PointerMoveEvent) {
+      _touchOffset = event.localPosition;
+    } else {
+      _touchOffset = null;
+    }
+
+    markNeedsPaint();
+  }
+}

+ 0 - 0
example/lib/demo/demo3.dart


+ 11 - 86
example/lib/main.dart

@@ -1,5 +1,8 @@
 import 'package:flutter/material.dart';
 
+import 'demo/demo1.dart';
+import 'demo/demo2.dart';
+
 void main() {
   runApp(const MyApp());
 }
@@ -7,109 +10,31 @@ void main() {
 class MyApp extends StatelessWidget {
   const MyApp({Key? key}) : super(key: key);
 
-  // This widget is the root of your application.
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       title: 'Flutter Demo',
       theme: ThemeData(
-        // This is the theme of your application.
-        //
-        // Try running your application with "flutter run". You'll see the
-        // application has a blue toolbar. Then, without quitting the app, try
-        // changing the primarySwatch below to Colors.green and then invoke
-        // "hot reload" (press "r" in the console where you ran "flutter run",
-        // or simply save your changes to "hot reload" in a Flutter IDE).
-        // Notice that the counter didn't reset back to zero; the application
-        // is not restarted.
         primarySwatch: Colors.blue,
       ),
-      home: const MyHomePage(title: 'Flutter Demo Home Page'),
+      home: const _TabDemo(),
     );
   }
 }
 
-class MyHomePage extends StatefulWidget {
-  const MyHomePage({Key? key, required this.title}) : super(key: key);
-
-  // This widget is the home page of your application. It is stateful, meaning
-  // that it has a State object (defined below) that contains fields that affect
-  // how it looks.
-
-  // This class is the configuration for the state. It holds the values (in this
-  // case the title) provided by the parent (in this case the App widget) and
-  // used by the build method of the State. Fields in a Widget subclass are
-  // always marked "final".
-
-  final String title;
+class _TabDemo extends StatefulWidget {
+  const _TabDemo({Key? key}) : super(key: key);
 
   @override
-  State<MyHomePage> createState() => _MyHomePageState();
+  State<_TabDemo> createState() => _TabDemoState();
 }
 
-class _MyHomePageState extends State<MyHomePage> {
-  int _counter = 0;
-
-  void _incrementCounter() {
-    setState(() {
-      // This call to setState tells the Flutter framework that something has
-      // changed in this State, which causes it to rerun the build method below
-      // so that the display can reflect the updated values. If we changed
-      // _counter without calling setState(), then the build method would not be
-      // called again, and so nothing would appear to happen.
-      _counter++;
-    });
-  }
-
+class _TabDemoState extends State<_TabDemo> {
   @override
   Widget build(BuildContext context) {
-    // This method is rerun every time setState is called, for instance as done
-    // by the _incrementCounter method above.
-    //
-    // The Flutter framework has been optimized to make rerunning build methods
-    // fast, so that you can just rebuild anything that needs updating rather
-    // than having to individually change instances of widgets.
-    return Scaffold(
-      appBar: AppBar(
-        // Here we take the value from the MyHomePage object that was created by
-        // the App.build method, and use it to set our appbar title.
-        title: Text(widget.title),
-      ),
-      body: Center(
-        // Center is a layout widget. It takes a single child and positions it
-        // in the middle of the parent.
-        child: Column(
-          // Column is also a layout widget. It takes a list of children and
-          // arranges them vertically. By default, it sizes itself to fit its
-          // children horizontally, and tries to be as tall as its parent.
-          //
-          // Invoke "debug painting" (press "p" in the console, choose the
-          // "Toggle Debug Paint" action from the Flutter Inspector in Android
-          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
-          // to see the wireframe for each widget.
-          //
-          // Column has various properties to control how it sizes itself and
-          // how it positions its children. Here we use mainAxisAlignment to
-          // center the children vertically; the main axis here is the vertical
-          // axis because Columns are vertical (the cross axis would be
-          // horizontal).
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: <Widget>[
-            const Text(
-              'You have pushed the button this many times:',
-            ),
-            Text(
-              '$_counter',
-              style: Theme.of(context).textTheme.headline4,
-            ),
-          ],
-        ),
-      ),
-      floatingActionButton: FloatingActionButton(
-        onPressed: _incrementCounter,
-        tooltip: 'Increment',
-        child: const Icon(Icons.add),
-      ), // This trailing comma makes auto-formatting nicer for build methods.
+    return const Scaffold(
+      appBar: null,
+      body: Demo2(),
     );
   }
 }

+ 28 - 0
example/pubspec.lock

@@ -48,11 +48,39 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.7.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.8.1"
   sky_engine:
     dependency: transitive
     description: flutter
     source: sdk
     version: "0.0.99"
+  sqflite:
+    dependency: transitive
+    description:
+      name: sqflite
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.2"
+  sqflite_common:
+    dependency: transitive
+    description:
+      name: sqflite_common
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.1+1"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.0"
   typed_data:
     dependency: transitive
     description:

+ 7 - 7
lib/luojigou_thinking_core.dart

@@ -1,9 +1,9 @@
-///
+/// 逻辑狗思维芯模块代码库
 library luojigou_thinking_core;
 
-import 'dao.dart';
-import 'data.dart';
-import 'model.dart';
-import 'page.dart';
-import 'view.dart';
-import 'widget.dart';
+export 'dao.dart';
+export 'data.dart';
+export 'model.dart';
+export 'page.dart';
+export 'view.dart';
+export 'widget.dart';

+ 40 - 0
lib/src/utils/db_statement.dart

@@ -0,0 +1,40 @@
+part of 'db_util.dart';
+
+/// 数据库创建执行语句
+
+const String _createTable = '''
+CREATE TABLE "record" (
+  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
+  "level1_id" text,
+  "level2_id" text,
+  "level3_id" text,
+  "level4_id" text,
+  "record_user" text,
+  "record_text" text,
+  "record_image" text,
+  "record_video" text
+);
+''';
+
+const String _update1_2 = "";
+
+String _insertRecord(DataModel model) {
+  return '''
+INSERT INTO record 
+(level1_id,level2_id,level3_id,level4_id,record_user,record_text,record_image,record_video) 
+VALUES
+('${model.level1Id}','${model.level2Id}','${model.level3Id}','${model.level4Id}','${model.userId}','${model.text}','${model.image}','${model.video}');
+  ''';
+}
+
+String _deleteRecord(int id) {
+  return '''
+DELETE FROM record WHERE id = $id;
+''';
+}
+
+String _selectAll() {
+  return '''
+  SELECT * FROM record;
+  ''';
+}

+ 120 - 0
lib/src/utils/db_util.dart

@@ -0,0 +1,120 @@
+library db_util;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:sqflite/sqflite.dart';
+
+part 'db_statement.dart';
+
+class DataModel {
+  String? level1Id;
+  String? level2Id;
+  String? level3Id;
+  String? level4Id;
+
+  String userId;
+  String? text;
+  List<String>? image;
+  String? video;
+
+  DataModel({
+    this.level1Id,
+    this.level2Id,
+    this.level3Id,
+    this.level4Id,
+    required this.userId,
+    this.text,
+    this.image,
+    this.video,
+  });
+
+  static DataModel? fromJson(Map<String, Object?> json) {
+    if (json['record_user'] == null) return null;
+
+    return DataModel(
+      userId: json['record_user'] as String,
+      level1Id: null,
+    );
+  }
+
+  @override
+  String toString() {
+    return '''level1Id:$level1Id , level2Id:$level2Id ,level1Id:$level1Id , level2Id:$level2Id ,
+              userId:$userId ,  text:$text , image: $image , video: $video
+     ''';
+  }
+}
+
+class DBUtil {
+  static const int databaseVersion = 1;
+
+  late Database _database;
+  late Completer _completer;
+
+  DBUtil._();
+
+  Future<void> init() async {
+    _completer = Completer();
+    try {
+      var path = (await getDatabasesPath()) +
+          Platform.pathSeparator +
+          "thinking_core.db";
+
+      _database = await openDatabase(
+        path,
+        version: databaseVersion,
+        onCreate: _create,
+      );
+
+      _completer.complete(true);
+    } catch (e) {
+      _completer.completeError(e);
+    }
+  }
+
+  Future<void> close() async {
+    try {
+      if (_completer.isCompleted) {
+        _database.close();
+      }
+    } catch (e) {}
+  }
+
+  Future<void> insert(DataModel model) async {
+    await _database.rawInsert(_insertRecord(model));
+  }
+
+  Future<void> delete(int id) async {
+    await _database.rawDelete(_deleteRecord(id));
+  }
+
+  Future<List<DataModel>> selectAll() async {
+    List list = await _database.rawQuery(_selectAll());
+
+    return list.map((e) => DataModel.fromJson(e)).nonNull().toList();
+  }
+
+  FutureOr<void> _create(Database db, int version) async {
+    await _upgrade(db, version, databaseVersion);
+  }
+
+  FutureOr<void> _upgrade(Database db, int oldVersion, int newVersion) async {
+    for (int version = oldVersion; version <= newVersion; version++) {
+      if (version == 1) db.execute(_createTable);
+      // if(version == 2) db.execute()
+    }
+  }
+}
+
+final DBUtil dbUtil = DBUtil._();
+
+extension NonNullFilter<T> on Iterable<T?> {
+  Iterable<T> nonNull() {
+    List<T> list = [];
+    for (var element in this) {
+      if (element != null) list.add(element);
+    }
+    return list;
+  }
+}

+ 153 - 0
lib/src/widget/put_away.dart

@@ -0,0 +1,153 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/scheduler.dart';
+
+/// 控制折叠的widget
+///
+/// PutAwayDirection: 折叠的方向
+/// rtl 从右向左折叠
+/// btt 从下到上折叠
+enum PutAwayDirection { rtl, btt }
+
+class PutAway extends SingleChildRenderObjectWidget {
+  final PutAwayDirection direction;
+  final bool isPutAway;
+
+  const PutAway({
+    Key? key,
+    required this.direction,
+    this.isPutAway = false,
+    required Widget child,
+  }) : super(key: key, child: child);
+
+  @override
+  _RenderPutAway createRenderObject(BuildContext context) {
+    return _RenderPutAway(direction, isPutAway);
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderPutAway renderObject) {
+    renderObject
+      ..direction = direction
+      ..isPutAway = isPutAway;
+  }
+}
+
+class _RenderPutAway extends RenderBox
+    with RenderObjectWithChildMixin<RenderBox>
+    implements TickerProvider {
+  _RenderPutAway(this._direction, this._isPutAway);
+
+  PutAwayDirection _direction;
+
+  set direction(PutAwayDirection value) {
+    if (_direction != value) {
+      _direction = value;
+      markNeedsLayout();
+    }
+  }
+
+  bool _isPutAway = false;
+
+  set isPutAway(bool value) {
+    if (_isPutAway != value) {
+      _isPutAway = value;
+      if (_isPutAway) {
+        _controller.forward();
+      } else {
+        _controller.reverse();
+      }
+    }
+  }
+
+  late AnimationController _controller;
+  late Animation _animation;
+
+  @override
+  Ticker createTicker(TickerCallback onTick) => Ticker(onTick);
+
+  @override
+  void attach(covariant PipelineOwner owner) {
+    super.attach(owner);
+    _controller = AnimationController(
+      vsync: this,
+      duration: const Duration(milliseconds: 200),
+    );
+    _animation = CurvedAnimation(
+      parent: _controller,
+      curve: Curves.easeIn,
+      reverseCurve: Curves.easeOut,
+    );
+    _controller.addListener(markNeedsLayout);
+  }
+
+  Size _childSize = Size.zero;
+
+  @override
+  void performLayout() {
+    child!.layout(constraints, parentUsesSize: true);
+    _childSize = child!.size;
+    switch (_direction) {
+      case PutAwayDirection.rtl:
+        size = Size(
+          child!.size.width * (1 - _animation.value),
+          child!.size.height,
+        );
+        break;
+      case PutAwayDirection.btt:
+        size = Size(
+          child!.size.width,
+          child!.size.height * (1 - _animation.value),
+        );
+        break;
+      default:
+        size = child!.size;
+    }
+  }
+
+  @override
+  void detach() {
+    _controller.stop();
+    _controller.removeListener(markNeedsLayout);
+    _controller.dispose();
+    super.detach();
+  }
+
+  @override
+  bool hitTestSelf(Offset position) => true;
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+    return child!.hitTest(result, position: position);
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    super.paint(context, offset);
+    if (_animation.value == 0.0) {
+      context.paintChild(child!, offset);
+      return;
+    }
+    if (_animation.value == 1.0) return;
+    switch (_direction) {
+      case PutAwayDirection.rtl:
+        layer = context.pushClipRect(
+          needsCompositing,
+          offset,
+          Offset.zero & size,
+          child!.paint,
+          oldLayer: layer as ClipRectLayer?,
+        );
+        break;
+      case PutAwayDirection.btt:
+        layer = context.pushClipRect(
+          needsCompositing,
+          offset,
+          Offset.zero & size,
+          child!.paint,
+          oldLayer: layer as ClipRectLayer?,
+        );
+        break;
+    }
+  }
+}

+ 286 - 0
lib/src/widget/radar_chart.dart

@@ -0,0 +1,286 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'dart:math' as math;
+
+class DimensionInfo {
+  final int maxValue;
+  final String title;
+
+  DimensionInfo({
+    required this.maxValue,
+    required this.title,
+  });
+}
+
+class RadarChart extends LeafRenderObjectWidget {
+  const RadarChart({Key? key}) : super(key: key);
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderRadarChart();
+  }
+}
+
+class _RenderRadarChart extends RenderBox {
+  @override
+  void performLayout() {
+    size = constraints.constrain(Size.infinite);
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    return constraints.constrain(Size.infinite);
+  }
+
+  @override
+  bool hitTestSelf(Offset position) {
+    return size.contains(position);
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    super.paint(context, offset);
+    var canvas = context.canvas;
+    ChartPainter painter = RadarPainter();
+    painter._size = size;
+    canvas.save();
+    canvas.translate(offset.dx, offset.dy);
+    int count = canvas.getSaveCount();
+    painter.paintBackground(canvas);
+    painter.paintExtraInfo(canvas);
+    painter.paintDataInfo(canvas);
+    painter.paintForeground(canvas);
+    assert(count == canvas.getSaveCount(),
+        "Canvas save() or saveLayer() call times isn't equals restore()");
+    canvas.restore();
+  }
+}
+
+abstract class ChartPainter {
+  late Size _size;
+
+  Size get size => _size;
+
+  void paintBackground(Canvas canvas);
+
+  void paintExtraInfo(Canvas canvas);
+
+  void paintDataInfo(Canvas canvas);
+
+  void paintForeground(Canvas canvas);
+
+  Path dashPath(Path path) {
+    final PathMetrics pms = path.computeMetrics();
+    const double partLength = 4;
+
+    final newPath = Path();
+
+    for (var pm in pms) {
+      final int count = pm.length ~/ partLength;
+      for (int i = 0; i < count; i += 2) {
+        newPath.addPath(
+            pm.extractPath(partLength * i, partLength * (i + 1)), Offset.zero);
+      }
+    }
+    return newPath;
+  }
+}
+
+class RadarPainter extends ChartPainter {
+  int maxDimension = 3;
+
+  double circle = 2 * math.pi;
+
+  double get maxRadius => size.shortestSide / 2;
+
+  List<String> titles = ["早期", "中期", "后期"];
+
+  // List<String> title2s = ["早期一二三", "中期四五六", "后期七八九", "晚期三六九", "初期二五八"];
+  List<String> title2s = ["早期一二三", "中期四五六", "后期七八九", "晚期"];
+  // List<String> title2s = ["早期一", "中期四五", "后期七八九"];
+  List<List<int>> data = [
+    [0, 0, 0, 0],
+    [0, 0, 0, 0],
+    [0, 0, 0, 0],
+    [0, 0, 1, 3],
+    [3, 1, 1, 1],
+  ];
+
+  List<Color> colors = [
+    Colors.orange,
+    Colors.indigo,
+    Colors.yellowAccent,
+    Colors.deepPurpleAccent,
+    Colors.pink,
+  ];
+
+  @override
+  void paintBackground(Canvas canvas) {
+    canvas.save();
+    canvas.translate(size.width / 2, size.height / 2);
+    final paint = Paint()
+      ..color = Colors.white
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = 0.5;
+
+    for (int i = 0; i < maxDimension; i++) {
+      var radius = maxRadius / maxDimension * i;
+      var path = Path()
+        ..addOval(Rect.fromCircle(center: Offset.zero, radius: radius));
+      canvas.drawPath(dashPath(path), paint);
+      // canvas.drawCircle(Offset.zero, radius, paint);
+    }
+
+    canvas.drawCircle(Offset.zero, maxRadius, paint..strokeWidth = 1);
+    canvas.restore();
+  }
+
+  @override
+  void paintDataInfo(Canvas canvas) {
+    canvas.save();
+    canvas.translate(size.width / 2, size.height / 2);
+    double strokeWidth = 0;
+
+    for (int i = 0; i < data.length; i++) {
+      var path = Path();
+      strokeWidth = 2 * (i + 1);
+      for (int j = 0; j < data[i].length; j++) {
+        var radius = circle / data[i].length * (j) - math.pi / 2;
+
+        if (j == 0) {
+          path.moveTo(
+            data[i][j] == 0
+                ? strokeWidth * math.cos(radius)
+                : maxRadius * data[i][j] / maxDimension * math.cos(radius),
+            data[i][j] == 0
+                ? strokeWidth * math.sin(radius)
+                : maxRadius * data[i][j] / maxDimension * math.sin(radius),
+          );
+        } else {
+          path.lineTo(
+            data[i][j] == 0
+                ? strokeWidth * math.cos(radius)
+                : maxRadius * data[i][j] / maxDimension * math.cos(radius),
+            data[i][j] == 0
+                ? strokeWidth * math.sin(radius)
+                : maxRadius * data[i][j] / maxDimension * math.sin(radius),
+          );
+        }
+      }
+      path.close();
+      canvas.drawPath(
+          path,
+          Paint()
+            ..color = colors[i].withOpacity(0.2 + 0.8 * ((i + 1) / data.length))
+            ..style = PaintingStyle.stroke
+            ..strokeWidth = 2);
+    }
+
+    canvas.restore();
+  }
+
+  @override
+  void paintExtraInfo(Canvas canvas) {
+    canvas.save();
+    canvas.translate(size.width / 2, size.height / 2);
+
+    for (int i = 0; i < maxDimension; i++) {
+      canvas.save();
+
+      var radius = maxRadius / maxDimension * (i + 1);
+
+      canvas.translate(
+          radius * math.cos(math.pi / 4), radius * math.sin(math.pi / 4));
+      canvas.drawCircle(Offset.zero, 4, Paint()..color = Colors.red);
+
+      TextPainter tp = TextPainter();
+      tp.textDirection = TextDirection.ltr;
+      tp.text = TextSpan(
+        text: titles[i],
+        style: const TextStyle(
+            color: Color(0xFFB9BEE6),
+            fontSize: 7,
+            height: 10 / 7,
+            backgroundColor: Colors.green),
+      );
+      tp.layout(maxWidth: double.infinity);
+
+      var tpSize = tp.size;
+
+      canvas.translate(-tpSize.width / 2, -tpSize.height / 2);
+
+      // canvas.drawRect(
+      //   Rect.fromCenter(
+      //     center: Offset.zero,
+      //     width: tpSize.width + 15,
+      //     height: tpSize.height + 15,
+      //   ),
+      //   Paint()..color = Colors.green,
+      // );
+
+      tp.paint(canvas, Offset.zero);
+
+      canvas.restore();
+    }
+    canvas.restore();
+
+    canvas.save();
+    canvas.translate(size.width / 2, size.height / 2);
+
+    for (int i = 0; i < title2s.length; i++) {
+      canvas.save();
+
+      /// 计算文字大小
+      TextPainter tp = TextPainter();
+      tp.textDirection = TextDirection.ltr;
+      tp.text = TextSpan(
+        text: title2s[i],
+        style: const TextStyle(
+            color: Color(0xFFB9BEE6),
+            fontSize: 7,
+            height: 10 / 7,
+            backgroundColor: Colors.red),
+      );
+      tp.layout(maxWidth: double.infinity);
+
+      /// 旋转角度
+      var radius = circle / title2s.length * i - math.pi / 2;
+
+      canvas.drawLine(
+        Offset.zero,
+        Offset(maxRadius * math.cos(radius), maxRadius * math.sin(radius)),
+        Paint()
+          ..color = Colors.yellow
+          ..strokeWidth = 1,
+      );
+
+      double textRadius = math.sqrt(
+        math.pow(tp.width / 2 * math.cos(radius), 2) +
+            math.pow(tp.height / 2 * math.sin(radius), 2),
+      );
+
+      canvas.translate(
+        (maxRadius + 10 + textRadius) * math.cos(radius),
+        (maxRadius + 10 + textRadius) * math.sin(radius),
+      );
+
+      // canvas.drawCircle(Offset.zero, 14, Paint()..color = Colors.blue);
+
+      var tpSize = tp.size;
+
+      canvas.translate(
+        -tpSize.width / 2,
+        -tpSize.height / 2,
+      );
+      //
+      tp.paint(canvas, Offset.zero);
+      canvas.restore();
+    }
+    canvas.restore();
+  }
+
+  @override
+  void paintForeground(Canvas canvas) {
+  }
+}

+ 136 - 0
lib/src/widget/reverse_flex.dart

@@ -0,0 +1,136 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+/// 这里靠继承来实现绝大部分功能
+class ReverseRow extends _ReverseFlex {
+  ReverseRow({
+    Key? key,
+    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>[],
+  }) : super(
+          key: key,
+          children: children,
+          direction: Axis.horizontal,
+          mainAxisSize: mainAxisSize,
+          mainAxisAlignment: mainAxisAlignment,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+        );
+}
+
+class ReverseColumn extends _ReverseFlex {
+  ReverseColumn({
+    Key? key,
+    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>[],
+  }) : super(
+          key: key,
+          children: children,
+          direction: Axis.vertical,
+          mainAxisSize: mainAxisSize,
+          mainAxisAlignment: mainAxisAlignment,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+        );
+}
+
+class _ReverseFlex extends Flex {
+  _ReverseFlex({
+    Key? key,
+    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>[],
+  }) : super(
+          children: children,
+          key: key,
+          direction: direction,
+          mainAxisAlignment: mainAxisAlignment,
+          mainAxisSize: mainAxisSize,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+        );
+
+  @override
+  RenderFlex createRenderObject(BuildContext context) {
+    return _RenderReverseFlex(
+      direction: direction,
+      mainAxisAlignment: mainAxisAlignment,
+      mainAxisSize: mainAxisSize,
+      crossAxisAlignment: crossAxisAlignment,
+      textDirection: getEffectiveTextDirection(context),
+      verticalDirection: verticalDirection,
+      textBaseline: textBaseline,
+      clipBehavior: clipBehavior,
+    );
+  }
+
+  @override
+  void updateRenderObject(
+      BuildContext context, covariant RenderFlex renderObject) {
+    renderObject
+      ..direction = direction
+      ..mainAxisAlignment = mainAxisAlignment
+      ..mainAxisSize = mainAxisSize
+      ..crossAxisAlignment = crossAxisAlignment
+      ..textDirection = getEffectiveTextDirection(context)
+      ..verticalDirection = verticalDirection
+      ..textBaseline = textBaseline
+      ..clipBehavior = clipBehavior;
+  }
+}
+
+class _RenderReverseFlex extends RenderFlex {
+  _RenderReverseFlex({
+    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,
+    Clip clipBehavior = Clip.none,
+  }) : super(
+          children: children,
+          direction: direction,
+          mainAxisSize: mainAxisSize,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+          clipBehavior: clipBehavior,
+          mainAxisAlignment: mainAxisAlignment,
+        );
+
+  @override
+  void defaultPaint(PaintingContext context, Offset offset) {
+    RenderBox? child = lastChild;
+    while (child != null) {
+      final FlexParentData childParentData =
+          child.parentData! as FlexParentData;
+      context.paintChild(child, childParentData.offset + offset);
+      child = childParentData.previousSibling;
+    }
+  }
+}

+ 59 - 0
lib/src/widget/trapezoid.dart

@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+class TrapezoidBox extends MultiChildRenderObjectWidget {
+  TrapezoidBox({
+    Key? key,
+    required List<Widget> children,
+  }) : super(key: key, children: children);
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderTrapezoidBox();
+  }
+}
+
+class TrapezoidParentData extends ContainerBoxParentData<RenderBox> {
+  Offset a = Offset.zero;
+  Offset b = Offset.zero;
+  Offset c = Offset.zero;
+  Offset d = Offset.zero;
+}
+
+class _RenderTrapezoidBox extends RenderBox
+    with
+        ContainerRenderObjectMixin<RenderBox, TrapezoidParentData>,
+        RenderBoxContainerDefaultsMixin<RenderBox, TrapezoidParentData> {
+  @override
+  void setupParentData(covariant RenderObject child) {
+    if (child.parentData is! TrapezoidParentData) {
+      child.parentData = TrapezoidParentData();
+    }
+  }
+
+  @override
+  void performLayout() {
+
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    for (var child in getChildrenAsList()) {
+      _paintChild(context, offset, child);
+    }
+  }
+
+  void _paintChild(PaintingContext context, Offset offset, RenderBox child) {
+    TrapezoidParentData parentData = child.parentData as TrapezoidParentData;
+
+    context.canvas.save();
+    Path path = Path();
+    path.moveTo(parentData.a.dx, parentData.a.dy);
+    path.lineTo(parentData.b.dx, parentData.b.dy);
+    path.lineTo(parentData.c.dx, parentData.c.dy);
+    path.lineTo(parentData.d.dx, parentData.d.dy);
+    path.close();
+    context.canvas.clipPath(path);
+    context.canvas.restore();
+  }
+}

+ 1392 - 0
lib/src/widget/vertical_tab_bar.dart

@@ -0,0 +1,1392 @@
+import 'dart:math' as math;
+import 'dart:ui' show lerpDouble;
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart' show DragStartBehavior;
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+import 'package:vector_math/vector_math_64.dart' show Vector4;
+
+const double _kTabHeight = 46.0;
+const double _kTextAndIconTabHeight = 72.0;
+
+class _IndicatorPainter extends CustomPainter {
+  _IndicatorPainter({
+    required this.controller,
+    required this.indicator,
+    required this.indicatorSize,
+    required this.tabKeys,
+    required _IndicatorPainter? old,
+    required this.indicatorPadding,
+  })  : assert(controller != null),
+        assert(indicator != null),
+        super(repaint: controller.animation) {
+    if (old != null) {
+      saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
+    }
+  }
+
+  final TabController controller;
+  final Decoration indicator;
+  final TabBarIndicatorSize? indicatorSize;
+  final EdgeInsetsGeometry indicatorPadding;
+  final List<GlobalKey> tabKeys;
+
+  // _currentTabOffsets and _currentTextDirection are set each time TabBar
+  // layout is completed. These values can be null when TabBar contains no
+  // tabs, since there are nothing to lay out.
+  List<double>? _currentTabOffsets;
+  TextDirection? _currentTextDirection;
+
+  Rect? _currentRect;
+  BoxPainter? _painter;
+  bool _needsPaint = false;
+
+  void markNeedsPaint() {
+    _needsPaint = true;
+  }
+
+  void dispose() {
+    _painter?.dispose();
+  }
+
+  void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
+    _currentTabOffsets = tabOffsets;
+    _currentTextDirection = textDirection;
+  }
+
+  // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
+  // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
+  int get maxTabIndex => _currentTabOffsets!.length - 2;
+
+  double centerOf(int tabIndex) {
+    assert(_currentTabOffsets != null);
+    assert(_currentTabOffsets!.isNotEmpty);
+    assert(tabIndex >= 0);
+    assert(tabIndex <= maxTabIndex);
+    return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) /
+        2.0;
+  }
+
+  Rect indicatorRect(Size tabBarSize, int tabIndex) {
+    assert(_currentTabOffsets != null);
+    assert(_currentTextDirection != null);
+    assert(_currentTabOffsets!.isNotEmpty);
+    assert(tabIndex >= 0);
+    assert(tabIndex <= maxTabIndex);
+    double tabLeft, tabRight;
+    switch (_currentTextDirection!) {
+      case TextDirection.rtl:
+        tabLeft = _currentTabOffsets![tabIndex + 1];
+        tabRight = _currentTabOffsets![tabIndex];
+        break;
+      case TextDirection.ltr:
+        tabLeft = _currentTabOffsets![tabIndex];
+        tabRight = _currentTabOffsets![tabIndex + 1];
+        break;
+    }
+
+    if (indicatorSize == TabBarIndicatorSize.label) {
+      final double tabWidth = tabKeys[tabIndex].currentContext!.size!.width;
+      final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
+      tabLeft += delta;
+      tabRight -= delta;
+    }
+
+    final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection);
+    final Rect rect =
+        Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height);
+
+    if (!(rect.size >= insets.collapsedSize)) {
+      throw FlutterError(
+        'indicatorPadding insets should be less than Tab Size\n'
+        'Rect Size : ${rect.size}, Insets: ${insets.toString()}',
+      );
+    }
+    return insets.deflateRect(rect);
+  }
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    _needsPaint = false;
+    _painter ??= indicator.createBoxPainter(markNeedsPaint);
+
+    final double index = controller.index.toDouble();
+    final double value = controller.animation!.value;
+    final bool ltr = index > value;
+    final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex);
+    final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex);
+    final Rect fromRect = indicatorRect(size, from);
+    final Rect toRect = indicatorRect(size, to);
+    _currentRect = Rect.lerp(fromRect, toRect, (value - from).abs());
+    assert(_currentRect != null);
+
+    final ImageConfiguration configuration = ImageConfiguration(
+      size: _currentRect!.size,
+      textDirection: _currentTextDirection,
+    );
+    _painter!.paint(canvas, _currentRect!.topLeft, configuration);
+  }
+
+  @override
+  bool shouldRepaint(_IndicatorPainter old) {
+    return _needsPaint ||
+        controller != old.controller ||
+        indicator != old.indicator ||
+        tabKeys.length != old.tabKeys.length ||
+        (!listEquals(_currentTabOffsets, old._currentTabOffsets)) ||
+        _currentTextDirection != old._currentTextDirection;
+  }
+}
+
+class _TabStyle extends AnimatedWidget {
+  const _TabStyle({
+    Key? key,
+    required Animation<double> animation,
+    required this.selected,
+    required this.labelColor,
+    required this.unselectedLabelColor,
+    required this.labelStyle,
+    required this.unselectedLabelStyle,
+    required this.child,
+  }) : super(key: key, listenable: animation);
+
+  final TextStyle? labelStyle;
+  final TextStyle? unselectedLabelStyle;
+  final bool selected;
+  final Color? labelColor;
+  final Color? unselectedLabelColor;
+  final Widget child;
+
+  @override
+  Widget build(BuildContext context) {
+    final ThemeData themeData = Theme.of(context);
+    final TabBarTheme tabBarTheme = TabBarTheme.of(context);
+    final Animation<double> animation = listenable as Animation<double>;
+
+    // To enable TextStyle.lerp(style1, style2, value), both styles must have
+    // the same value of inherit. Force that to be inherit=true here.
+    final TextStyle defaultStyle = (labelStyle ??
+            tabBarTheme.labelStyle ??
+            themeData.primaryTextTheme.bodyText1!)
+        .copyWith(inherit: true);
+    final TextStyle defaultUnselectedStyle = (unselectedLabelStyle ??
+            tabBarTheme.unselectedLabelStyle ??
+            labelStyle ??
+            themeData.primaryTextTheme.bodyText1!)
+        .copyWith(inherit: true);
+    final TextStyle textStyle = selected
+        ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)!
+        : TextStyle.lerp(
+            defaultUnselectedStyle, defaultStyle, animation.value)!;
+
+    final Color selectedColor = labelColor ??
+        tabBarTheme.labelColor ??
+        themeData.primaryTextTheme.bodyText1!.color!;
+    final Color unselectedColor = unselectedLabelColor ??
+        tabBarTheme.unselectedLabelColor ??
+        selectedColor.withAlpha(0xB2); // 70% alpha
+    final Color color = selected
+        ? Color.lerp(selectedColor, unselectedColor, animation.value)!
+        : Color.lerp(unselectedColor, selectedColor, animation.value)!;
+
+    return DefaultTextStyle(
+      style: textStyle.copyWith(color: color),
+      child: IconTheme.merge(
+        data: IconThemeData(
+          size: 24.0,
+          color: color,
+        ),
+        child: child,
+      ),
+    );
+  }
+}
+
+typedef _LayoutCallback = void Function(
+    List<double> xOffsets, TextDirection textDirection, double width);
+
+class _TabLabelBarRenderer extends RenderFlex {
+  _TabLabelBarRenderer({
+    List<RenderBox>? children,
+    required Axis direction,
+    required MainAxisSize mainAxisSize,
+    required MainAxisAlignment mainAxisAlignment,
+    required CrossAxisAlignment crossAxisAlignment,
+    required TextDirection textDirection,
+    required VerticalDirection verticalDirection,
+    required this.onPerformLayout,
+  })  : assert(onPerformLayout != null),
+        assert(textDirection != null),
+        super(
+          children: children,
+          direction: direction,
+          mainAxisSize: mainAxisSize,
+          mainAxisAlignment: mainAxisAlignment,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+        );
+
+  _LayoutCallback onPerformLayout;
+
+  @override
+  void performLayout() {
+    super.performLayout();
+    // xOffsets will contain childCount+1 values, giving the offsets of the
+    // leading edge of the first tab as the first value, of the leading edge of
+    // the each subsequent tab as each subsequent value, and of the trailing
+    // edge of the last tab as the last value.
+    RenderBox? child = firstChild;
+    final List<double> xOffsets = <double>[];
+    while (child != null) {
+      final FlexParentData childParentData =
+          child.parentData! as FlexParentData;
+      xOffsets.add(childParentData.offset.dx);
+      assert(child.parentData == childParentData);
+      child = childParentData.nextSibling;
+    }
+    assert(textDirection != null);
+    switch (textDirection!) {
+      case TextDirection.rtl:
+        xOffsets.insert(0, size.width);
+        break;
+      case TextDirection.ltr:
+        xOffsets.add(size.width);
+        break;
+    }
+    onPerformLayout(xOffsets, textDirection!, size.width);
+  }
+}
+
+class _TabLabelBar extends Flex {
+  _TabLabelBar({
+    Key? key,
+    List<Widget> children = const <Widget>[],
+    required this.onPerformLayout,
+  }) : super(
+          key: key,
+          children: children,
+          direction: Axis.horizontal,
+          mainAxisSize: MainAxisSize.max,
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          verticalDirection: VerticalDirection.down,
+        );
+
+  final _LayoutCallback onPerformLayout;
+
+  @override
+  RenderFlex createRenderObject(BuildContext context) {
+    return _TabLabelBarRenderer(
+      direction: direction,
+      mainAxisAlignment: mainAxisAlignment,
+      mainAxisSize: mainAxisSize,
+      crossAxisAlignment: crossAxisAlignment,
+      textDirection: getEffectiveTextDirection(context)!,
+      verticalDirection: verticalDirection,
+      onPerformLayout: onPerformLayout,
+    );
+  }
+
+  @override
+  void updateRenderObject(
+      BuildContext context, _TabLabelBarRenderer renderObject) {
+    super.updateRenderObject(context, renderObject);
+    renderObject.onPerformLayout = onPerformLayout;
+  }
+}
+
+double _indexChangeProgress(TabController controller) {
+  final double controllerValue = controller.animation!.value;
+  final double previousIndex = controller.previousIndex.toDouble();
+  final double currentIndex = controller.index.toDouble();
+
+  // The controller's offset is changing because the user is dragging the
+  // TabBarView's PageView to the left or right.
+  if (!controller.indexIsChanging)
+    return (currentIndex - controllerValue).abs().clamp(0.0, 1.0);
+
+  // The TabController animation's value is changing from previousIndex to currentIndex.
+  return (controllerValue - currentIndex).abs() /
+      (currentIndex - previousIndex).abs();
+}
+
+class _ChangeAnimation extends Animation<double>
+    with AnimationWithParentMixin<double> {
+  _ChangeAnimation(this.controller);
+
+  final TabController controller;
+
+  @override
+  Animation<double> get parent => controller.animation!;
+
+  @override
+  void removeStatusListener(AnimationStatusListener listener) {
+    if (controller.animation != null) super.removeStatusListener(listener);
+  }
+
+  @override
+  void removeListener(VoidCallback listener) {
+    if (controller.animation != null) super.removeListener(listener);
+  }
+
+  @override
+  double get value => _indexChangeProgress(controller);
+}
+
+class _DragAnimation extends Animation<double>
+    with AnimationWithParentMixin<double> {
+  _DragAnimation(this.controller, this.index);
+
+  final TabController controller;
+  final int index;
+
+  @override
+  Animation<double> get parent => controller.animation!;
+
+  @override
+  void removeStatusListener(AnimationStatusListener listener) {
+    if (controller.animation != null) super.removeStatusListener(listener);
+  }
+
+  @override
+  void removeListener(VoidCallback listener) {
+    if (controller.animation != null) super.removeListener(listener);
+  }
+
+  @override
+  double get value {
+    assert(!controller.indexIsChanging);
+    final double controllerMaxValue = (controller.length - 1).toDouble();
+    final double controllerValue =
+        controller.animation!.value.clamp(0.0, controllerMaxValue);
+    return (controllerValue - index.toDouble()).abs().clamp(0.0, 1.0);
+  }
+}
+
+// This class, and TabBarScrollController, only exist to handle the case
+// where a scrollable TabBar has a non-zero initialIndex. In that case we can
+// only compute the scroll position's initial scroll offset (the "correct"
+// pixels value) after the TabBar viewport width and scroll limits are known.
+class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
+  _TabBarScrollPosition({
+    required ScrollPhysics physics,
+    required ScrollContext context,
+    required ScrollPosition? oldPosition,
+    required this.tabBar,
+  }) : super(
+          physics: physics,
+          context: context,
+          initialPixels: null,
+          oldPosition: oldPosition,
+        );
+
+  final _VerticalTabBarState tabBar;
+
+  bool? _initialViewportDimensionWasZero;
+
+  @override
+  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
+    bool result = true;
+    if (_initialViewportDimensionWasZero != true) {
+      // If the viewport never had a non-zero dimension, we just want to jump
+      // to the initial scroll position to avoid strange scrolling effects in
+      // release mode: In release mode, the viewport temporarily may have a
+      // dimension of zero before the actual dimension is calculated. In that
+      // scenario, setting the actual dimension would cause a strange scroll
+      // effect without this guard because the super call below would starts a
+      // ballistic scroll activity.
+      assert(viewportDimension != null);
+      _initialViewportDimensionWasZero = viewportDimension != 0.0;
+      correctPixels(tabBar._initialScrollOffset(
+          viewportDimension, minScrollExtent, maxScrollExtent));
+      result = false;
+    }
+    return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &&
+        result;
+  }
+}
+
+// This class, and TabBarScrollPosition, only exist to handle the case
+// where a scrollable TabBar has a non-zero initialIndex.
+class _TabBarScrollController extends ScrollController {
+  _TabBarScrollController(this.tabBar);
+
+  final _VerticalTabBarState tabBar;
+
+  @override
+  ScrollPosition createScrollPosition(ScrollPhysics physics,
+      ScrollContext context, ScrollPosition? oldPosition) {
+    return _TabBarScrollPosition(
+      physics: physics,
+      context: context,
+      oldPosition: oldPosition,
+      tabBar: tabBar,
+    );
+  }
+}
+
+class _TabItemWrapper extends SingleChildRenderObjectWidget {
+  const _TabItemWrapper({
+    Key? key,
+    required Widget child,
+  }) : super(key: key, child: child);
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderTabItemWrapper();
+  }
+}
+
+class _RenderTabItemWrapper extends RenderBox
+    with RenderObjectWithChildMixin<RenderBox> {
+  int _itemCount = 0;
+
+  @override
+  void attach(covariant PipelineOwner owner) {
+    super.attach(owner);
+    assert(() {
+      _itemCount += 1;
+      return true;
+    }());
+  }
+
+  @override
+  void adoptChild(covariant RenderObject child) {
+    super.adoptChild(child);
+    assert(() {
+      _itemCount += 1;
+      return true;
+    }());
+  }
+
+  @override
+  void dropChild(covariant RenderObject child) {
+    assert(() {
+      _itemCount -= 1;
+      return true;
+    }());
+
+    super.dropChild(child);
+  }
+
+  @override
+  void detach() {
+    assert(() {
+      _itemCount -= 1;
+      return true;
+    }());
+    super.detach();
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    assert(_itemCount > 0);
+
+    if (child == null) return Size.zero;
+    return ChildLayoutHelper.dryLayoutChild(child!, constraints);
+  }
+
+  @override
+  void performLayout() {
+    assert(_itemCount > 0);
+    if (child == null) size = Size.zero;
+
+    child!.layout(constraints, parentUsesSize: true);
+    size = child!.size;
+  }
+
+  @override
+  bool hitTest(BoxHitTestResult result, {required Offset position}) {
+    return child?.hitTest(result, position: position) ?? false;
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    super.paint(context, offset);
+    if (child == null) return;
+    context.paintChild(child!, offset);
+  }
+}
+
+class _TabWrapper extends SingleChildRenderObjectWidget {
+  const _TabWrapper({
+    Key? key,
+    required Widget child,
+  }) : super(key: key, child: child);
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderTabWrapper();
+  }
+}
+
+class _RenderTabWrapper extends RenderBox
+    with RenderObjectWithChildMixin<RenderBox> {
+  bool _attached = false;
+  bool _hasAdoptChild = false;
+
+  @override
+  void attach(covariant PipelineOwner owner) {
+    super.attach(owner);
+    assert(!_attached);
+    assert(() {
+      _attached = true;
+      return true;
+    }());
+    if (_attached) {
+      if (child != null) {
+        debugValidateChild(child!);
+        assert(() {
+          _hasAdoptChild = true;
+          return true;
+        }());
+      }
+    }
+  }
+
+  @override
+  void detach() {
+    assert(_attached);
+    assert(() {
+      _attached = false;
+      return true;
+    }());
+    if (_hasAdoptChild) {
+      assert(() {
+        _hasAdoptChild = false;
+        return true;
+      }());
+    }
+    super.detach();
+  }
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    if (child == null) return 0.0;
+    return super.computeMinIntrinsicWidth(height);
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    if (child == null) return 0.0;
+    return super.computeMinIntrinsicHeight(width);
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    if (child == null) return 1.0;
+    return super.computeMaxIntrinsicWidth(height);
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    if (child == null) return 1.0;
+    return super.computeMaxIntrinsicHeight(width);
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    if (child == null) {
+      return Size.zero;
+    }
+
+    return ChildLayoutHelper.dryLayoutChild(child!, constraints).flipped;
+  }
+
+  @override
+  void performLayout() {
+    if (child == null) {
+      size = Size.zero;
+    } else {
+      child!.layout(
+        constraints.flipped,
+        parentUsesSize: true,
+      );
+      size = constraints.constrain(child!.size.flipped);
+    }
+  }
+
+  @override
+  bool hitTestSelf(Offset position) => size.contains(position);
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+    if (child == null) return false;
+
+    return result.addWithPaintTransform(
+      transform: _getTransform(),
+      position: position,
+      hitTest: (BoxHitTestResult result, Offset? position) {
+        return child!.hitTest(result, position: position!);
+      },
+    );
+  }
+
+  final LayerHandle<TransformLayer> _layerHandle =
+      LayerHandle<TransformLayer>();
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    super.paint(context, offset);
+
+    if (_layerHandle.layer != null && _layerHandle.layer is TransformLayer) {
+      context.pushTransform(
+        needsCompositing,
+        offset,
+        _getTransform(),
+        _paint,
+        oldLayer: _layerHandle.layer!,
+      );
+    } else {
+      _layerHandle.layer = context.pushTransform(
+        needsCompositing,
+        offset,
+        _getTransform(),
+        _paint,
+      );
+    }
+  }
+
+  void _paint(PaintingContext context, Offset offset) {
+    context.paintChild(child!, offset);
+  }
+
+  Matrix4 _getTransform() {
+    double cdc = 2 * (constraints.minWidth + 1);
+    double csc = constraints.minHeight - constraints.minHeight - 1;
+
+    Matrix4 m1 = Matrix4.zero();
+    Matrix4 m2 = Matrix4.zero();
+
+    m1.row0 = Vector4(cdc, 0, 0, 0);
+    m1.row1 = Vector4(0, cdc, 0, 0);
+    m1.row2 = Vector4(0, 0, csc, 0);
+    m1.row3 = Vector4(0, 0, 0, csc);
+
+    m2.row3 = Vector4(0, 0, 0, -1);
+    m2.row2 = Vector4(0, 0, -1, 0);
+    m2.row1 = Vector4(0, 0.5, 0, 0);
+    m2.row0 = Vector4(0.5, 0, 0, 0);
+
+    Matrix4 m = m1 * m2;
+    double a = m.trace() / 4, b = a / 16;
+
+    m.rotateZ(-math.pi / 2);
+    m.translate(-size.height, 0, 0);
+
+    m.multiply(Matrix4(-a, a, a, a, a, -a, a, a, a, a, -a, a, a, a, a, -a));
+    m.multiply(Matrix4(-b, b, b, b, b, -b, b, b, b, b, -b, b, b, b, b, -b));
+    return m;
+  }
+}
+
+class _ClipRectTab extends SingleChildRenderObjectWidget {
+  const _ClipRectTab({
+    Key? key,
+    required Widget child,
+  }) : super(key: key, child: child);
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderClipRectTab();
+  }
+}
+
+class _RenderClipRectTab extends RenderBox
+    with RenderObjectWithChildMixin<_RenderTabWrapper> {
+  _RenderClipRectTab();
+
+  @override
+  _RenderTabWrapper? get child => super.child as _RenderTabWrapper;
+
+  @override
+  void performLayout() {
+    child!.layout(constraints, parentUsesSize: true);
+    size = child!.size;
+  }
+
+  @override
+  bool hitTest(BoxHitTestResult result, {required Offset position}) {
+    return child!.hitTest(result, position: position);
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    super.paint(context, offset);
+    context.pushClipRect(
+      needsCompositing,
+      offset,
+      Offset.zero & Size(size.width + 40, size.height),
+      child!.paint,
+    );
+  }
+}
+
+class VerticalTabBar extends StatefulWidget implements PreferredSizeWidget {
+  /// Creates a material design tab bar.
+  ///
+  /// The [tabs] argument must not be null and its length must match the [controller]'s
+  /// [TabController.length].
+  ///
+  /// If a [TabController] is not provided, then there must be a
+  /// [DefaultTabController] ancestor.
+  ///
+  /// The [indicatorWeight] parameter defaults to 2, and must not be null.
+  ///
+  /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
+  ///
+  /// If [indicator] is not null or provided from [TabBarTheme],
+  /// then [indicatorWeight], [indicatorPadding], and [indicatorColor] are ignored.
+  const VerticalTabBar({
+    Key? key,
+    required this.tabs,
+    this.controller,
+    this.isScrollable = false,
+    this.padding,
+    this.indicatorColor,
+    this.automaticIndicatorColorAdjustment = true,
+    this.indicatorWeight = 2.0,
+    this.indicatorPadding = EdgeInsets.zero,
+    this.indicator,
+    this.indicatorSize,
+    this.labelColor,
+    this.labelStyle,
+    this.labelPadding,
+    this.unselectedLabelColor,
+    this.unselectedLabelStyle,
+    this.dragStartBehavior = DragStartBehavior.start,
+    this.overlayColor,
+    this.mouseCursor,
+    this.onTap,
+    this.physics,
+  })  : assert(tabs != null),
+        assert(isScrollable != null),
+        assert(dragStartBehavior != null),
+        assert(indicator != null ||
+            (indicatorWeight != null && indicatorWeight > 0.0)),
+        assert(indicator != null || (indicatorPadding != null)),
+        super(key: key);
+
+  /// Typically a list of two or more [Tab] widgets.
+  ///
+  /// The length of this list must match the [controller]'s [TabController.length]
+  /// and the length of the [TabBarView.children] list.
+  final List<Widget> tabs;
+
+  /// This widget's selection and animation state.
+  ///
+  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
+  /// will be used.
+  final TabController? controller;
+
+  /// Whether this tab bar can be scrolled horizontally.
+  ///
+  /// If [isScrollable] is true, then each tab is as wide as needed for its label
+  /// and the entire [VerticalTabBar] is scrollable. Otherwise each tab gets an equal
+  /// share of the available space.
+  final bool isScrollable;
+
+  /// The amount of space by which to inset the tab bar.
+  ///
+  /// When [isScrollable] is false, this will yield the same result as if you had wrapped your
+  /// [VerticalTabBar] in a [Padding] widget. When [isScrollable] is true, the scrollable itself is inset,
+  /// allowing the padding to scroll with the tab bar, rather than enclosing it.
+  final EdgeInsetsGeometry? padding;
+
+  /// The color of the line that appears below the selected tab.
+  ///
+  /// If this parameter is null, then the value of the Theme's indicatorColor
+  /// property is used.
+  ///
+  /// If [indicator] is specified or provided from [TabBarTheme],
+  /// this property is ignored.
+  final Color? indicatorColor;
+
+  /// The thickness of the line that appears below the selected tab.
+  ///
+  /// The value of this parameter must be greater than zero and its default
+  /// value is 2.0.
+  ///
+  /// If [indicator] is specified or provided from [TabBarTheme],
+  /// this property is ignored.
+  final double indicatorWeight;
+
+  /// Padding for indicator.
+  /// This property will now no longer be ignored even if indicator is declared
+  /// or provided by [TabBarTheme]
+  ///
+  /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align
+  /// the indicator with the tab's text for [Tab] widgets and all but the
+  /// shortest [Tab.text] values.
+  ///
+  /// The default value of [indicatorPadding] is [EdgeInsets.zero].
+  final EdgeInsetsGeometry indicatorPadding;
+
+  /// Defines the appearance of the selected tab indicator.
+  ///
+  /// If [indicator] is specified or provided from [TabBarTheme],
+  /// the [indicatorColor], and [indicatorWeight] properties are ignored.
+  ///
+  /// The default, underline-style, selected tab indicator can be defined with
+  /// [UnderlineTabIndicator].
+  ///
+  /// The indicator's size is based on the tab's bounds. If [indicatorSize]
+  /// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space
+  /// occupied by the tab in the tab bar. If [indicatorSize] is
+  /// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as
+  /// the tab widget itself.
+  final Decoration? indicator;
+
+  /// Whether this tab bar should automatically adjust the [indicatorColor].
+  ///
+  /// If [automaticIndicatorColorAdjustment] is true,
+  /// then the [indicatorColor] will be automatically adjusted to [Colors.white]
+  /// when the [indicatorColor] is same as [Material.color] of the [Material] parent widget.
+  final bool automaticIndicatorColorAdjustment;
+
+  /// Defines how the selected tab indicator's size is computed.
+  ///
+  /// The size of the selected tab indicator is defined relative to the
+  /// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab]
+  /// (the default) or relative to the bounds of the tab's widget if
+  /// [indicatorSize] is [TabBarIndicatorSize.label].
+  ///
+  /// The selected tab's location appearance can be refined further with
+  /// the [indicatorColor], [indicatorWeight], [indicatorPadding], and
+  /// [indicator] properties.
+  final TabBarIndicatorSize? indicatorSize;
+
+  /// The color of selected tab labels.
+  ///
+  /// Unselected tab labels are rendered with the same color rendered at 70%
+  /// opacity unless [unselectedLabelColor] is non-null.
+  ///
+  /// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s
+  /// bodyText1 text color is used.
+  final Color? labelColor;
+
+  /// The color of unselected tab labels.
+  ///
+  /// If this property is null, unselected tab labels are rendered with the
+  /// [labelColor] with 70% opacity.
+  final Color? unselectedLabelColor;
+
+  /// The text style of the selected tab labels.
+  ///
+  /// If [unselectedLabelStyle] is null, then this text style will be used for
+  /// both selected and unselected label styles.
+  ///
+  /// If this property is null, then the text style of the
+  /// [ThemeData.primaryTextTheme]'s bodyText1 definition is used.
+  final TextStyle? labelStyle;
+
+  /// The padding added to each of the tab labels.
+  ///
+  /// If there are few tabs with both icon and text and few
+  /// tabs with only icon or text, this padding is vertically
+  /// adjusted to provide uniform padding to all tabs.
+  ///
+  /// If this property is null, then kTabLabelPadding is used.
+  final EdgeInsetsGeometry? labelPadding;
+
+  /// The text style of the unselected tab labels.
+  ///
+  /// If this property is null, then the [labelStyle] value is used. If [labelStyle]
+  /// is null, then the text style of the [ThemeData.primaryTextTheme]'s
+  /// bodyText1 definition is used.
+  final TextStyle? unselectedLabelStyle;
+
+  /// Defines the ink response focus, hover, and splash colors.
+  ///
+  /// If non-null, it is resolved against one of [MaterialState.focused],
+  /// [MaterialState.hovered], and [MaterialState.pressed].
+  ///
+  /// [MaterialState.pressed] triggers a ripple (an ink splash), per
+  /// the current Material Design spec. The [overlayColor] doesn't map
+  /// a state to [InkResponse.highlightColor] because a separate highlight
+  /// is not used by the current design guidelines. See
+  /// https://material.io/design/interaction/states.html#pressed
+  ///
+  /// If the overlay color is null or resolves to null, then the default values
+  /// for [InkResponse.focusColor], [InkResponse.hoverColor], [InkResponse.splashColor]
+  /// will be used instead.
+  final MaterialStateProperty<Color?>? overlayColor;
+
+  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
+  final DragStartBehavior dragStartBehavior;
+
+  /// The cursor for a mouse pointer when it enters or is hovering over the
+  /// individual tab widgets.
+  ///
+  /// If this property is null, [SystemMouseCursors.click] will be used.
+  final MouseCursor? mouseCursor;
+
+  /// An optional callback that's called when the [VerticalTabBar] is tapped.
+  ///
+  /// The callback is applied to the index of the tab where the tap occurred.
+  ///
+  /// This callback has no effect on the default handling of taps. It's for
+  /// applications that want to do a little extra work when a tab is tapped,
+  /// even if the tap doesn't change the TabController's index. TabBar [onTap]
+  /// callbacks should not make changes to the TabController since that would
+  /// interfere with the default tap handler.
+  final ValueChanged<int>? onTap;
+
+  /// How the [VerticalTabBar]'s scroll view should respond to user input.
+  ///
+  /// For example, determines how the scroll view continues to animate after the
+  /// user stops dragging the scroll view.
+  ///
+  /// Defaults to matching platform conventions.
+  final ScrollPhysics? physics;
+
+  /// A size whose height depends on if the tabs have both icons and text.
+  ///
+  /// [AppBar] uses this size to compute its own preferred size.
+  @override
+  Size get preferredSize {
+    double maxHeight = _kTabHeight;
+    for (final Widget item in tabs) {
+      if (item is PreferredSizeWidget) {
+        final double itemHeight = item.preferredSize.height;
+        maxHeight = math.max(itemHeight, maxHeight);
+      }
+    }
+    return Size.fromHeight(maxHeight + indicatorWeight);
+  }
+
+  /// Returns whether the [VerticalTabBar] contains a tab with both text and icon.
+  ///
+  /// [VerticalTabBar] uses this to give uniform padding to all tabs in cases where
+  /// there are some tabs with both text and icon and some which contain only
+  /// text or icon.
+  bool get tabHasTextAndIcon {
+    for (final Widget item in tabs) {
+      if (item is PreferredSizeWidget) {
+        if (item.preferredSize.height == _kTextAndIconTabHeight) return true;
+      }
+    }
+    return false;
+  }
+
+  @override
+  State<VerticalTabBar> createState() => _VerticalTabBarState();
+}
+
+class _VerticalTabBarState extends State<VerticalTabBar> {
+  ScrollController? _scrollController;
+  TabController? _controller;
+  _IndicatorPainter? _indicatorPainter;
+  int? _currentIndex;
+  late double _tabStripWidth;
+  late List<GlobalKey> _tabKeys;
+
+  @override
+  void initState() {
+    super.initState();
+    // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
+    // the width of tab widget i. See _IndicatorPainter.indicatorRect().
+    _tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList();
+  }
+
+  Decoration get _indicator {
+    if (widget.indicator != null) return widget.indicator!;
+    final TabBarTheme tabBarTheme = TabBarTheme.of(context);
+    if (tabBarTheme.indicator != null) return tabBarTheme.indicator!;
+
+    Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
+    // ThemeData tries to avoid this by having indicatorColor avoid being the
+    // primaryColor. However, it's possible that the tab bar is on a
+    // Material that isn't the primaryColor. In that case, if the indicator
+    // color ends up matching the material's color, then this overrides it.
+    // When that happens, automatic transitions of the theme will likely look
+    // ugly as the indicator color suddenly snaps to white at one end, but it's
+    // not clear how to avoid that any further.
+    //
+    // The material's color might be null (if it's a transparency). In that case
+    // there's no good way for us to find out what the color is so we don't.
+    //
+    // TODO(xu-baolin): Remove automatic adjustment to white color indicator
+    // with a better long-term solution.
+    // https://github.com/flutter/flutter/pull/68171#pullrequestreview-517753917
+    if (widget.automaticIndicatorColorAdjustment &&
+        color.value == Material.of(context)?.color?.value) color = Colors.white;
+
+    return UnderlineTabIndicator(
+      borderSide: BorderSide(
+        width: widget.indicatorWeight,
+        color: color,
+      ),
+    );
+  }
+
+  // If the TabBar is rebuilt with a new tab controller, the caller should
+  // dispose the old one. In that case the old controller's animation will be
+  // null and should not be accessed.
+  bool get _controllerIsValid => _controller?.animation != null;
+
+  void _updateTabController() {
+    final TabController? newController =
+        widget.controller ?? DefaultTabController.of(context);
+    assert(() {
+      if (newController == null) {
+        throw FlutterError(
+          'No TabController for ${widget.runtimeType}.\n'
+          'When creating a ${widget.runtimeType}, you must either provide an explicit '
+          'TabController using the "controller" property, or you must ensure that there '
+          'is a DefaultTabController above the ${widget.runtimeType}.\n'
+          'In this case, there was neither an explicit controller nor a default controller.',
+        );
+      }
+      return true;
+    }());
+
+    if (newController == _controller) return;
+
+    if (_controllerIsValid) {
+      _controller!.animation!.removeListener(_handleTabControllerAnimationTick);
+      _controller!.removeListener(_handleTabControllerTick);
+    }
+    _controller = newController;
+    if (_controller != null) {
+      _controller!.animation!.addListener(_handleTabControllerAnimationTick);
+      _controller!.addListener(_handleTabControllerTick);
+      _currentIndex = _controller!.index;
+    }
+  }
+
+  void _initIndicatorPainter() {
+    _indicatorPainter = !_controllerIsValid
+        ? null
+        : _IndicatorPainter(
+            controller: _controller!,
+            indicator: _indicator,
+            indicatorSize:
+                widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
+            indicatorPadding: widget.indicatorPadding,
+            tabKeys: _tabKeys,
+            old: _indicatorPainter,
+          );
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    assert(debugCheckHasMaterial(context));
+    _updateTabController();
+    _initIndicatorPainter();
+  }
+
+  @override
+  void didUpdateWidget(VerticalTabBar oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.controller != oldWidget.controller) {
+      _updateTabController();
+      _initIndicatorPainter();
+    } else if (widget.indicatorColor != oldWidget.indicatorColor ||
+        widget.indicatorWeight != oldWidget.indicatorWeight ||
+        widget.indicatorSize != oldWidget.indicatorSize ||
+        widget.indicator != oldWidget.indicator) {
+      _initIndicatorPainter();
+    }
+
+    if (widget.tabs.length > oldWidget.tabs.length) {
+      final int delta = widget.tabs.length - oldWidget.tabs.length;
+      _tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
+    } else if (widget.tabs.length < oldWidget.tabs.length) {
+      _tabKeys.removeRange(widget.tabs.length, oldWidget.tabs.length);
+    }
+  }
+
+  @override
+  void dispose() {
+    _indicatorPainter!.dispose();
+    if (_controllerIsValid) {
+      _controller!.animation!.removeListener(_handleTabControllerAnimationTick);
+      _controller!.removeListener(_handleTabControllerTick);
+    }
+    _controller = null;
+    // We don't own the _controller Animation, so it's not disposed here.
+    super.dispose();
+  }
+
+  int get maxTabIndex => _indicatorPainter!.maxTabIndex;
+
+  double _tabScrollOffset(
+      int index, double viewportWidth, double minExtent, double maxExtent) {
+    if (!widget.isScrollable) return 0.0;
+    double tabCenter = _indicatorPainter!.centerOf(index);
+    switch (Directionality.of(context)) {
+      case TextDirection.rtl:
+        tabCenter = _tabStripWidth - tabCenter;
+        break;
+      case TextDirection.ltr:
+        break;
+    }
+    return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent);
+  }
+
+  double _tabCenteredScrollOffset(int index) {
+    final ScrollPosition position = _scrollController!.position;
+    return _tabScrollOffset(index, position.viewportDimension,
+        position.minScrollExtent, position.maxScrollExtent);
+  }
+
+  double _initialScrollOffset(
+      double viewportWidth, double minExtent, double maxExtent) {
+    return _tabScrollOffset(
+        _currentIndex!, viewportWidth, minExtent, maxExtent);
+  }
+
+  void _scrollToCurrentIndex() {
+    final double offset =
+        _tabCenteredScrollOffset(_tabKeys.length - _currentIndex! - 1);
+    _scrollController!
+        .animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
+  }
+
+  void _scrollToControllerValue() {
+    final double? leadingPosition = _currentIndex! > 0
+        ? _tabCenteredScrollOffset(_currentIndex! - 1)
+        : null;
+    final double middlePosition = _tabCenteredScrollOffset(_currentIndex!);
+    final double? trailingPosition = _currentIndex! < maxTabIndex
+        ? _tabCenteredScrollOffset(_currentIndex! + 1)
+        : null;
+
+    final double index = _controller!.index.toDouble();
+    final double value = _controller!.animation!.value;
+    final double offset;
+    if (value == index - 1.0)
+      offset = leadingPosition ?? middlePosition;
+    else if (value == index + 1.0)
+      offset = trailingPosition ?? middlePosition;
+    else if (value == index)
+      offset = middlePosition;
+    else if (value < index)
+      offset = leadingPosition == null
+          ? middlePosition
+          : lerpDouble(middlePosition, leadingPosition, index - value)!;
+    else
+      offset = trailingPosition == null
+          ? middlePosition
+          : lerpDouble(middlePosition, trailingPosition, value - index)!;
+
+    _scrollController!.jumpTo(offset);
+  }
+
+  void _handleTabControllerAnimationTick() {
+    assert(mounted);
+    if (!_controller!.indexIsChanging && widget.isScrollable) {
+      // Sync the TabBar's scroll position with the TabBarView's PageView.
+      _currentIndex = _controller!.index;
+      _scrollToControllerValue();
+    }
+  }
+
+  void _handleTabControllerTick() {
+    if (_controller!.index != _currentIndex) {
+      _currentIndex = _controller!.index;
+      if (widget.isScrollable) _scrollToCurrentIndex();
+    }
+    setState(() {
+      // Rebuild the tabs after a (potentially animated) index change
+      // has completed.
+    });
+  }
+
+  // Called each time layout completes.
+  void _saveTabOffsets(
+      List<double> tabOffsets, TextDirection textDirection, double width) {
+    _tabStripWidth = width;
+    _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
+  }
+
+  void _handleTap(int index) {
+    assert(index >= 0 && index < widget.tabs.length);
+    _controller!.animateTo(index);
+    widget.onTap?.call(index);
+  }
+
+  Widget _buildStyledTab(
+      Widget child, bool selected, Animation<double> animation) {
+    return _TabStyle(
+      animation: animation,
+      selected: selected,
+      labelColor: widget.labelColor,
+      unselectedLabelColor: widget.unselectedLabelColor,
+      labelStyle: widget.labelStyle,
+      unselectedLabelStyle: widget.unselectedLabelStyle,
+      child: child,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    assert(debugCheckHasMaterialLocalizations(context));
+    assert(() {
+      if (_controller!.length != widget.tabs.length) {
+        throw FlutterError(
+          "Controller's length property (${_controller!.length}) does not match the "
+          "number of tabs (${widget.tabs.length}) present in TabBar's tabs property.",
+        );
+      }
+      return true;
+    }());
+    final MaterialLocalizations localizations =
+        MaterialLocalizations.of(context);
+    if (_controller!.length == 0) {
+      return Container(
+        height: _kTabHeight + widget.indicatorWeight,
+      );
+    }
+
+    final TabBarTheme tabBarTheme = TabBarTheme.of(context);
+
+    final List<Widget> wrappedTabs =
+        List<Widget>.generate(widget.tabs.length, (int index) {
+      const double verticalAdjustment =
+          (_kTextAndIconTabHeight - _kTabHeight) / 2.0;
+      EdgeInsetsGeometry? adjustedPadding;
+
+      if (widget.tabs[index] is PreferredSizeWidget) {
+        final PreferredSizeWidget tab =
+            widget.tabs[index] as PreferredSizeWidget;
+        if (widget.tabHasTextAndIcon &&
+            tab.preferredSize.height == _kTabHeight) {
+          if (widget.labelPadding != null || tabBarTheme.labelPadding != null) {
+            adjustedPadding = (widget.labelPadding ?? tabBarTheme.labelPadding!)
+                .add(const EdgeInsets.symmetric(vertical: verticalAdjustment));
+          } else {
+            adjustedPadding = const EdgeInsets.symmetric(
+                vertical: verticalAdjustment, horizontal: 16.0);
+          }
+        }
+      }
+
+      return Center(
+        heightFactor: 1.0,
+        child: Padding(
+          padding: adjustedPadding ??
+              widget.labelPadding ??
+              tabBarTheme.labelPadding ??
+              kTabLabelPadding,
+          child: KeyedSubtree(
+            key: _tabKeys[index],
+            child: widget.tabs[index],
+          ),
+        ),
+      );
+    });
+
+    // If the controller was provided by DefaultTabController and we're part
+    // of a Hero (typically the AppBar), then we will not be able to find the
+    // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213.
+    if (_controller != null) {
+      final int previousIndex = _controller!.previousIndex;
+
+      if (_controller!.indexIsChanging) {
+        // The user tapped on a tab, the tab controller's animation is running.
+        assert(_currentIndex != previousIndex);
+        final Animation<double> animation = _ChangeAnimation(_controller!);
+        wrappedTabs[_currentIndex!] =
+            _buildStyledTab(wrappedTabs[_currentIndex!], true, animation);
+        wrappedTabs[previousIndex] =
+            _buildStyledTab(wrappedTabs[previousIndex], false, animation);
+      } else {
+        // The user is dragging the TabBarView's PageView left or right.
+        final int tabIndex = _currentIndex!;
+        final Animation<double> centerAnimation =
+            _DragAnimation(_controller!, tabIndex);
+        wrappedTabs[tabIndex] =
+            _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
+        if (_currentIndex! > 0) {
+          final int tabIndex = _currentIndex! - 1;
+          final Animation<double> previousAnimation =
+              ReverseAnimation(_DragAnimation(_controller!, tabIndex));
+          wrappedTabs[tabIndex] =
+              _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);
+        }
+        if (_currentIndex! < widget.tabs.length - 1) {
+          final int tabIndex = _currentIndex! + 1;
+          final Animation<double> nextAnimation =
+              ReverseAnimation(_DragAnimation(_controller!, tabIndex));
+          wrappedTabs[tabIndex] =
+              _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);
+        }
+      }
+    }
+
+    // Add the tap handler to each tab. If the tab bar is not scrollable,
+    // then give all of the tabs equal flexibility so that they each occupy
+    // the same share of the tab bar's overall width.
+    final int tabCount = widget.tabs.length;
+    for (int index = 0; index < tabCount; index += 1) {
+      wrappedTabs[index] = GestureDetector(
+        onTap: () {
+          _handleTap(index);
+        },
+        child: _TabItemWrapper(
+          child: Padding(
+            padding: EdgeInsets.only(bottom: widget.indicatorWeight),
+            child: Stack(
+              children: <Widget>[
+                wrappedTabs[index],
+                Semantics(
+                  selected: index == _currentIndex,
+                  label: localizations.tabLabel(
+                      tabIndex: index + 1, tabCount: tabCount),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+      if (!widget.isScrollable) {
+        wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
+      }
+    }
+
+    Widget tabBar = CustomPaint(
+      painter: _indicatorPainter,
+      child: _TabStyle(
+        animation: kAlwaysDismissedAnimation,
+        selected: false,
+        labelColor: widget.labelColor,
+        unselectedLabelColor: widget.unselectedLabelColor,
+        labelStyle: widget.labelStyle,
+        unselectedLabelStyle: widget.unselectedLabelStyle,
+        child: _TabLabelBar(
+          onPerformLayout: _saveTabOffsets,
+          children: wrappedTabs,
+        ),
+      ),
+    );
+
+    if (widget.isScrollable) {
+      _scrollController ??= _TabBarScrollController(this);
+      tabBar = SingleChildScrollView(
+        clipBehavior: Clip.none,
+        dragStartBehavior: widget.dragStartBehavior,
+        reverse: true,
+        scrollDirection: Axis.horizontal,
+        controller: _scrollController,
+        padding: widget.padding,
+        physics: widget.physics,
+        child: tabBar,
+      );
+    } else if (widget.padding != null) {
+      tabBar = Padding(
+        padding: widget.padding!,
+        child: tabBar,
+      );
+    }
+
+    return _TabWrapper(
+      child: tabBar,
+    );
+  }
+}

+ 4 - 0
lib/widget.dart

@@ -0,0 +1,4 @@
+export 'src/widget/put_away.dart';
+export 'src/widget/radar_chart.dart';
+export 'src/widget/reverse_flex.dart';
+export 'src/widget/vertical_tab_bar.dart';

+ 28 - 0
pubspec.lock

@@ -41,11 +41,39 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.7.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.8.1"
   sky_engine:
     dependency: transitive
     description: flutter
     source: sdk
     version: "0.0.99"
+  sqflite:
+    dependency: "direct main"
+    description:
+      name: sqflite
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.2"
+  sqflite_common:
+    dependency: transitive
+    description:
+      name: sqflite_common
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.1+1"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.0"
   typed_data:
     dependency: transitive
     description:

+ 2 - 0
pubspec.yaml

@@ -11,6 +11,8 @@ dependencies:
   flutter:
     sdk: flutter
 
+  sqflite: ^2.0.2
+
 dev_dependencies:
   flutter_lints: ^1.0.0