瀏覽代碼

widget: vertical tab bar & reverse row & put away

zhaoyadi 3 年之前
父節點
當前提交
73cf3f6f3d
共有 5 個文件被更改,包括 375 次插入86 次删除
  1. 113 80
      example/lib/demo/demo1.dart
  2. 160 0
      lib/src/widget/put_away.dart
  3. 90 0
      lib/src/widget/reverse_row.dart
  4. 10 6
      lib/src/widget/vertical_tab_bar.dart
  5. 2 0
      lib/widget.dart

+ 113 - 80
example/lib/demo/demo1.dart

@@ -17,96 +17,127 @@ class _Demo1State extends State<Demo1> with SingleTickerProviderStateMixin {
   @override
   void initState() {
     super.initState();
-    _controller = TabController(length: 5, vsync: this);
+    _controller = TabController(length: 7, vsync: this);
   }
 
+  bool _value = false;
+
   @override
   Widget build(BuildContext context) {
-    return Row(
-      children: [
-        SizedBox(
-          width: 55,
-          height: double.infinity,
-          child: ColoredBox(
-            color: Color(0xFFC8DDFF),
-            child: Column(
-              children: [
-                SizedBox(
-                  width: double.infinity,
-                  height: 44 + MediaQuery.of(context).padding.top,
-                  child: const ColoredBox(
-                    color: Colors.teal,
+    return Scaffold(
+      backgroundColor: Color(0xFFC8DDFF),
+      body: ReverseRow(
+        children: [
+          PutAway(
+            direction: PutAwayDirection.rtl,
+            isPutAway: _value,
+            child: SizedBox(
+              width: 55,
+              height: double.infinity,
+              child: Column(
+                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,
-                    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('自由探索')),
+                  Expanded(
+                    child: VerticalTabBar(
+                      controller: _controller,
+                      isScrollable: true,
+                      padding: EdgeInsets.only(left: 40),
+                      labelStyle: const TextStyle(
+                        color: Color(0xFF75AAFF),
+                        fontSize: 16,
+                        height: 1,
                       ),
-                      Container(
-                        padding: EdgeInsets.symmetric(horizontal: 4),
-                        height: double.infinity,
-                        color: Colors.transparent,
-                        child: Center(child: const Text('思维探索')),
+                      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: () {
-              int lastIndex = _controller.index;
-              int nextIndex = (lastIndex + 1) % _controller.length;
-              _controller.animateTo(nextIndex);
-            },
-            child: ColoredBox(
-              color: Colors.transparent,
-              child: SizedBox.expand(),
+          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,
+                  ),
+                ),
+              ),
             ),
           ),
-        ),
-      ],
+        ],
+      ),
     );
   }
 }
@@ -142,8 +173,10 @@ class _BoxPainter extends BoxPainter {
 
     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);
+    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));
   }
 }

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

@@ -0,0 +1,160 @@
+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();
+      }
+    }
+  }
+
+  Ticker? _ticker;
+
+  @override
+  Ticker createTicker(TickerCallback onTick) {
+    assert(_ticker == null);
+    _ticker = Ticker(onTick);
+    return _ticker!;
+  }
+
+  late AnimationController _controller;
+  late Animation _animation;
+
+  @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;
+    }
+  }
+}

+ 90 - 0
lib/src/widget/reverse_row.dart

@@ -0,0 +1,90 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+/// 这里靠继承来实现绝大部分功能
+class ReverseRow extends Flex {
+  ReverseRow({
+    Key? key,
+    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
+    MainAxisSize mainAxisSize = MainAxisSize.max,
+    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
+    TextDirection? textDirection,
+    VerticalDirection verticalDirection = VerticalDirection.down,
+    TextBaseline?
+        textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
+    List<Widget> children = const <Widget>[],
+  }) : super(
+          children: children,
+          key: key,
+          mainAxisAlignment: mainAxisAlignment,
+          mainAxisSize: mainAxisSize,
+          direction: Axis.horizontal,
+          crossAxisAlignment: crossAxisAlignment,
+          textDirection: textDirection,
+          verticalDirection: verticalDirection,
+          textBaseline: textBaseline,
+        );
+
+  @override
+  _RenderReverseRow createRenderObject(BuildContext context) {
+    return _RenderReverseRow(
+      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 _RenderReverseRow extends RenderFlex {
+  _RenderReverseRow({
+    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;
+    }
+  }
+}

+ 10 - 6
lib/src/widget/vertical_tab_bar.dart

@@ -614,12 +614,15 @@ class _RenderTabWrapper extends RenderBox
 
   @override
   bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
-    var offset = Offset(
-      size.width - position.dy,
-      size.height - position.dx,
+    if (child == null) return false;
+
+    return result.addWithPaintTransform(
+      transform: _getTransform(),
+      position: position,
+      hitTest: (BoxHitTestResult result, Offset? position) {
+        return child!.hitTest(result, position: position!);
+      },
     );
-
-    return child!.hitTest(result, position: offset);
   }
 
   final LayerHandle<TransformLayer> _layerHandle =
@@ -1106,7 +1109,7 @@ class _VerticalTabBarState extends State<VerticalTabBar> {
   }
 
   void _scrollToCurrentIndex() {
-    final double offset = _tabCenteredScrollOffset(_currentIndex!);
+    final double offset = _tabCenteredScrollOffset(_tabKeys.length - _currentIndex! - 1);
     _scrollController!
         .animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
   }
@@ -1332,6 +1335,7 @@ class _VerticalTabBarState extends State<VerticalTabBar> {
     if (widget.isScrollable) {
       _scrollController ??= _TabBarScrollController(this);
       tabBar = SingleChildScrollView(
+        clipBehavior:Clip.none,
         dragStartBehavior: widget.dragStartBehavior,
         reverse: true,
         scrollDirection: Axis.horizontal,

+ 2 - 0
lib/widget.dart

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