Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent Scaffold rebuilds #38

Open
hwnprsd opened this issue Jun 13, 2020 · 12 comments
Open

Prevent Scaffold rebuilds #38

hwnprsd opened this issue Jun 13, 2020 · 12 comments

Comments

@hwnprsd
Copy link

hwnprsd commented Jun 13, 2020

I have a ton of animations in the scaffold widget of the InnerDrawer.
Most of them play only on buidl() and moving the drawer constantly calls rebuild on the scaffold widget.
This causes a very jittery experience.
I tried using AutomaticKeepAliveClientMixin and set the wantKeepAlive to true but, InnerDrawer somehow bypasses it

@StepanMynarik
Copy link

StepanMynarik commented Jun 15, 2020

Same problem. Every time I open or close the drawer, everything starts rebuilding like crazy :(

This happens in example_1.dart as well, so it should not be a faulty integration on my part.

@Dn-a
Copy link
Owner

Dn-a commented Jun 16, 2020

@StevenKek I need to check it out. I'll keep you posted

@StepanMynarik
Copy link

@Dn-a Thank you for the quick follow-up. This issue is particularly noticeable in our app due to a use of a WebView. Rebuilding WebView is just incredibly resource intensive.

@liuchuancong
Copy link

seem as me

@DirtyNative
Copy link

I am having the same issue, this should be fixed as soon as possible

@basilmariano
Copy link

same here :(

@virskor
Copy link

virskor commented Jul 14, 2020

remove setState on animate controller may help to solve this problem, using AnimatedBuilder to make sure scaffold not rebuild for heavy widget tree.

https://medium.com/flutter-community/flutter-laggy-animations-how-not-to-setstate-f2dd9873b8fc

This might be helpful.
Thanks for your great job
@Dn-a @d3fkon

@virskor
Copy link

virskor commented Jul 14, 2020

i already modified this bug for you all need. hope this can be helpful

@Dn-a @d3fkon @StepanMynarik @DirtyNative

// InnerDrawer is based on Drawer.
// The source code of the Drawer has been re-adapted for Inner Drawer.

// more details:
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/drawer.dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';

/// Signature for the callback that's called when a [InnerDrawer] is
/// opened or closed.
typedef InnerDrawerCallback = void Function(bool isOpened);

/// Signature for when a pointer that is in contact with the screen and moves to the right or left
/// values between 1 and 0
typedef InnerDragUpdateCallback = void Function(
    double value, InnerDrawerDirection direction);

/// The possible position of a [InnerDrawer].
enum InnerDrawerDirection {
  start,
  end,
}

/// Animation type of a [InnerDrawer].
enum InnerDrawerAnimation {
  static,
  linear,
  quadratic,
}

//width before initState
const double _kWidth = 400;
const double _kMinFlingVelocity = 365.0;
const double _kEdgeDragWidth = 25.0;
const Duration _kBaseSettleDuration = Duration(milliseconds: 200);

class InnerDrawer extends StatefulWidget {
  const InnerDrawer(
      {GlobalKey key,
      this.leftChild,
      this.rightChild,
      @required this.scaffold,
      this.leftOffset = 0.4,
      this.rightOffset = 0.4,
      this.leftScale = 1,
      this.rightScale = 1,
      this.offset,
      this.scale,
      this.proportionalChildArea = true,
      this.borderRadius = 0,
      this.onTapClose = false,
      this.tapScaffoldEnabled = false,
      this.swipe = true,
      this.duration,
      this.velocity = 1,
      this.boxShadow,
      this.colorTransitionChild,
      this.colorTransitionScaffold,
      this.leftAnimationType = InnerDrawerAnimation.static,
      this.rightAnimationType = InnerDrawerAnimation.static,
      this.backgroundDecoration,
      this.innerDrawerCallback,
      this.onDragUpdate})
      : assert(leftChild != null || rightChild != null),
        assert(scaffold != null),
        super(key: key);

  /// Left child
  final Widget leftChild;

  /// Right child
  final Widget rightChild;

  /// A Scaffold is generally used but you are free to use other widgets
  final Widget scaffold;

  /// DEPRECATED:
  /// Use `offset` field. Will be removed in 0.6.0
  ///
  /// Left offset of [InnerDrawer] width; (default 0.4)
  final double leftOffset;

  /// DEPRECATED:
  /// Use `offset` field. Will be removed in 0.6.0
  ///
  /// Right offset of [InnerDrawer] width; default 0.4
  final double rightOffset;

  /// When the [InnerDrawer] is open, it's possible to set the offset of each of the four cardinal directions
  final IDOffset offset;

  /// DEPRECATED:
  /// Use `scale` field. Will be removed in 0.6.0
  ///
  /// When the left [InnerDrawer] is open
  /// Values between 1 and 0. (default 1)
  final double leftScale;

  /// DEPRECATED:
  /// Use `scale` field. Will be removed in 0.6.0
  ///
  /// When the right [InnerDrawer] is open
  /// Values between 1 and 0. (default 1)
  final double rightScale;

  /// When the [InnerDrawer] is open to the left or to the right
  /// values between 1 and 0. (default 1)
  final IDOffset scale;

  /// The proportionalChild Area = true dynamically sets the width based on the selected offset.
  /// On false it leaves the width at 100% of the screen
  final bool proportionalChildArea;

  /// edge radius when opening the scaffold - (defalut 0)
  final double borderRadius;

  /// Closes the open scaffold
  final bool tapScaffoldEnabled;

  /// Closes the open scaffold
  final bool onTapClose;

  /// activate or deactivate the swipe. NOTE: when deactivate, onTap Close is implicitly activated
  final bool swipe;

  /// duration animation controller
  final Duration duration;

  /// possibility to set the opening and closing velocity
  final double velocity;

  /// BoxShadow of scaffold open
  final List<BoxShadow> boxShadow;

  ///Color of gradient background
  final Color colorTransitionChild;

  ///Color of gradient background
  final Color colorTransitionScaffold;

  /// Static or Linear or Quadratic
  final InnerDrawerAnimation leftAnimationType;

  /// Static or Linear or Quadratic
  final InnerDrawerAnimation rightAnimationType;

  /// Color of the main background
  final BoxDecoration backgroundDecoration;

  /// Optional callback that is called when a [InnerDrawer] is open or closed.
  final InnerDrawerCallback innerDrawerCallback;

  /// when a pointer that is in contact with the screen and moves to the right or left
  final InnerDragUpdateCallback onDragUpdate;

  @override
  InnerDrawerState createState() => InnerDrawerState();
}

class InnerDrawerState extends State<InnerDrawer>
    with SingleTickerProviderStateMixin {
  ColorTween _colorTransitionChild =
      ColorTween(begin: Colors.transparent, end: Colors.black54);
  ColorTween _colorTransitionScaffold =
      ColorTween(begin: Colors.black54, end: Colors.transparent);

  double _initWidth = _kWidth;
  Orientation _orientation = Orientation.portrait;
  InnerDrawerDirection _position;

  @override
  void initState() {
    _position = widget.leftChild != null
        ? InnerDrawerDirection.start
        : InnerDrawerDirection.end;

    _controller = AnimationController(
        value: 1,
        duration: widget.duration ?? _kBaseSettleDuration,
        vsync: this)
      ..addListener(_animationChanged)
      ..addStatusListener(_animationStatusChanged);
    super.initState();
  }

  @override
  void dispose() {
    _historyEntry?.remove();
    _controller.dispose();
    _focusScopeNode.dispose();
    super.dispose();
  }

  void _animationChanged() {
    if (widget.colorTransitionChild != null)
      _colorTransitionChild = ColorTween(
          begin: widget.colorTransitionChild.withOpacity(0.0),
          end: widget.colorTransitionChild);

    if (widget.colorTransitionScaffold != null)
      _colorTransitionScaffold = ColorTween(
          begin: widget.colorTransitionScaffold,
          end: widget.colorTransitionScaffold.withOpacity(0.0));

    if (widget.onDragUpdate != null && _controller.value < 1) {
      widget.onDragUpdate((1 - _controller.value), _position);
    }
  }

  LocalHistoryEntry _historyEntry;
  final FocusScopeNode _focusScopeNode = FocusScopeNode();

  void _ensureHistoryEntry() {
    if (_historyEntry == null) {
      final ModalRoute<dynamic> route = ModalRoute.of(context);
      if (route != null) {
        _historyEntry = LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
        route.addLocalHistoryEntry(_historyEntry);
        FocusScope.of(context).setFirstFocus(_focusScopeNode);
      }
    }
  }

  void _animationStatusChanged(AnimationStatus status) {
    final bool opened = _controller.value < 0.5 ? true : false;
    switch (status) {
      case AnimationStatus.reverse:
        break;
      case AnimationStatus.forward:
        break;
      case AnimationStatus.dismissed:
        if (_previouslyOpened != opened) {
          _previouslyOpened = opened;
          if (widget.innerDrawerCallback != null)
            widget.innerDrawerCallback(opened);
        }
        _ensureHistoryEntry();
        break;
      case AnimationStatus.completed:
        if (_previouslyOpened != opened) {
          _previouslyOpened = opened;
          if (widget.innerDrawerCallback != null)
            widget.innerDrawerCallback(opened);
        }
        _historyEntry?.remove();
        _historyEntry = null;
    }
  }

  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }

  AnimationController _controller;

  void _handleDragDown(DragDownDetails details) {
    _controller.stop();
    //_ensureHistoryEntry();
  }

  final GlobalKey _drawerKey = GlobalKey();

  double get _width {
    return _initWidth;
  }

  double get _velocity {
    return widget.velocity;
  }

  /// get width of screen after initState
  void _updateWidth() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final RenderBox box = _drawerKey.currentContext.findRenderObject();
      //final RenderBox box = context.findRenderObject();
      if (box != null && box.size != null && box.size.width > 300)
        setState(() {
          _initWidth = box.size.width;
        });
    });
  }

  bool _previouslyOpened = false;

  void _move(DragUpdateDetails details) {
    double delta = details.primaryDelta / _width;

    if (delta > 0 && _controller.value == 1 && widget.leftChild != null)
      _position = InnerDrawerDirection.start;
    else if (delta < 0 && _controller.value == 1 && widget.rightChild != null)
      _position = InnerDrawerDirection.end;

    //TEMP
    final double left =
        widget.offset != null ? widget.offset.left : widget.leftOffset;
    final double right =
        widget.offset != null ? widget.offset.right : widget.rightOffset;

    double offset = _position == InnerDrawerDirection.start ? left : right;

    double ee = 1;
    if (offset <= 0.2)
      ee = 1.7;
    else if (offset <= 0.4)
      ee = 1.2;
    else if (offset <= 0.6) ee = 1.05;

    offset = 1 -
        pow(offset / ee,
            1 / 2); //(num.parse(pow(offset/2,1/3).toStringAsFixed(1)));

    switch (_position) {
      case InnerDrawerDirection.end:
        break;
      case InnerDrawerDirection.start:
        delta = -delta;
        break;
    }
    switch (Directionality.of(context)) {
      case TextDirection.rtl:
        _controller.value -= delta + (delta * offset);
        break;
      case TextDirection.ltr:
        _controller.value += delta + (delta * offset);
        break;
    }

    final bool opened = _controller.value < 0.5 ? true : false;
    if (opened != _previouslyOpened && widget.innerDrawerCallback != null)
      widget.innerDrawerCallback(opened);
    _previouslyOpened = opened;
  }

  void _settle(DragEndDetails details) {
    if (_controller.isDismissed) return;
    if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
      double visualVelocity =
          (details.velocity.pixelsPerSecond.dx + _velocity) / _width;

      switch (_position) {
        case InnerDrawerDirection.end:
          break;
        case InnerDrawerDirection.start:
          visualVelocity = -visualVelocity;
          break;
      }
      switch (Directionality.of(context)) {
        case TextDirection.rtl:
          _controller.fling(velocity: -visualVelocity);
          break;
        case TextDirection.ltr:
          _controller.fling(velocity: visualVelocity);
          break;
      }
    } else if (_controller.value < 0.5) {
      open();
    } else {
      close();
    }
  }

  void open({InnerDrawerDirection direction}) {
    if (direction != null) _position = direction;
    _controller.fling(velocity: -_velocity);
  }

  void close({InnerDrawerDirection direction}) {
    if (direction != null) _position = direction;
    _controller.fling(velocity: _velocity);
  }

  /// Open or Close InnerDrawer
  void toggle({InnerDrawerDirection direction}) {
    if (_previouslyOpened)
      close(direction: direction);
    else
      open(direction: direction);
  }

  final GlobalKey _gestureDetectorKey = GlobalKey();

  /// Outer Alignment
  AlignmentDirectional get _drawerOuterAlignment {
    switch (_position) {
      case InnerDrawerDirection.start:
        return AlignmentDirectional.centerEnd;
      case InnerDrawerDirection.end:
        return AlignmentDirectional.centerStart;
    }
    return null;
  }

  /// Inner Alignment
  AlignmentDirectional get _drawerInnerAlignment {
    switch (_position) {
      case InnerDrawerDirection.start:
        return AlignmentDirectional.centerStart;
      case InnerDrawerDirection.end:
        return AlignmentDirectional.centerEnd;
    }
    return null;
  }

  /// returns the left or right animation type based on InnerDrawerDirection
  InnerDrawerAnimation get _animationType {
    return _position == InnerDrawerDirection.start
        ? widget.leftAnimationType
        : widget.rightAnimationType;
  }

  /// returns the left or right scale based on InnerDrawerDirection
  double get _scaleFactor {
    //TEMP
    final double left =
        widget.scale != null ? widget.scale.left : widget.leftScale;
    final double right =
        widget.scale != null ? widget.scale.right : widget.rightScale;

    return _position == InnerDrawerDirection.start ? left : right;
  }

  /// returns the left or right offset based on InnerDrawerDirection
  double get _offset {
    //TEMP
    final double left =
        widget.offset != null ? widget.offset.left : widget.leftOffset;
    final double right =
        widget.offset != null ? widget.offset.right : widget.rightOffset;

    return _position == InnerDrawerDirection.start ? left : right;
  }

  /// return width with specific offset
  double get _widthWithOffset {
    return (_width / 2) - (_width / 2) * _offset;
    //return _width  - _width * _offset;
  }

  /// return swipe
  bool get _swipe {
    //if( _offset == 0 ) return false;
    return widget.swipe;
  }

  /// return widget with specific animation
  Widget _animatedChild() {
    final Widget container = Container(
      width: widget.proportionalChildArea ? _width - _widthWithOffset : _width,
      height: MediaQuery.of(context).size.height,
      child: _position == InnerDrawerDirection.start
          ? widget.leftChild
          : widget.rightChild,
    );

    switch (_animationType) {
      case InnerDrawerAnimation.linear:
        return Align(
          alignment: _drawerOuterAlignment,
          widthFactor: 1 - (_controller.value),
          child: container,
        );
      case InnerDrawerAnimation.quadratic:
        return Align(
          alignment: _drawerOuterAlignment,
          widthFactor: 1 - (_controller.value / 2),
          child: container,
        );
      default:
        return container;
    }
  }

  /// Trigger Area
  Widget _trigger(AlignmentDirectional alignment, Widget child) {
    assert(alignment != null);
    final bool drawerIsStart = _position == InnerDrawerDirection.start;
    final EdgeInsets padding = MediaQuery.of(context).padding;
    double dragAreaWidth = drawerIsStart ? padding.left : padding.right;

    if (Directionality.of(context) == TextDirection.rtl)
      dragAreaWidth = drawerIsStart ? padding.right : padding.left;
    dragAreaWidth = max(dragAreaWidth, _kEdgeDragWidth);

    if (_controller.status == AnimationStatus.completed &&
        _swipe &&
        child != null)
      return Align(
        alignment: alignment,
        child: Container(color: Colors.transparent, width: dragAreaWidth),
      );
    else
      return null;
  }

  ///Disable the scaffolding tap when the drawer is open
  Widget _invisibleCover() {
    final Container container = Container(
      color: _colorTransitionScaffold.evaluate(_controller),
    );
    if (_controller.value != 1.0 && !widget.tapScaffoldEnabled)
      return BlockSemantics(
        child: GestureDetector(
          // On Android, the back button is used to dismiss a modal.
          excludeFromSemantics: defaultTargetPlatform == TargetPlatform.android,
          onTap: widget.onTapClose || !_swipe ? close : null,
          child: Semantics(
            label: MaterialLocalizations.of(context)?.modalBarrierDismissLabel,
            child: container,
          ),
        ),
      );
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget scaffold) {
        //assert(debugCheckHasMaterialLocalizations(context));

        /// initialize the correct width
        if (_initWidth == 400 ||
            MediaQuery.of(context).orientation != _orientation) {
          _updateWidth();
          _orientation = MediaQuery.of(context).orientation;
        }

        /// wFactor depends of offset and is used by the second Align that contains the Scaffold
        final double offset = 0.5 - _offset * 0.5;
        //final double offset = 1 - _offset * 1;
        final double wFactor = (_controller.value * (1 - offset)) + offset;

        return Container(
          decoration: widget.backgroundDecoration ??
              BoxDecoration(
                color: Theme.of(context).backgroundColor,
              ),
          child: Stack(
            alignment: _drawerInnerAlignment,
            children: <Widget>[
              FocusScope(node: _focusScopeNode, child: _animatedChild()),
              GestureDetector(
                key: _gestureDetectorKey,
                onTap: () {},
                onHorizontalDragDown: _swipe ? _handleDragDown : null,
                onHorizontalDragUpdate: _swipe ? _move : null,
                onHorizontalDragEnd: _swipe ? _settle : null,
                excludeFromSemantics: true,
                child: RepaintBoundary(
                  child: Stack(
                    children: <Widget>[
                      ///Gradient
                      Container(
                        width: _controller.value == 0 ||
                                _animationType == InnerDrawerAnimation.linear
                            ? 0
                            : null,
                        color: _colorTransitionChild.evaluate(_controller),
                      ),
                      Align(
                        alignment: _drawerOuterAlignment,
                        child: Align(
                            alignment: _drawerInnerAlignment,
                            widthFactor: wFactor,
                            child: Builder(
                              builder: (BuildContext context) {
                                assert(widget.borderRadius >= 0);
                                Widget scaffoldChild = Stack(
                                  children: <Widget>[
                                    scaffold,
                                    _invisibleCover() ?? const SizedBox()
                                  ],
                                );

                                Widget container = Container(
                                    key: _drawerKey,
                                    decoration: BoxDecoration(
                                        borderRadius: BorderRadius.circular(
                                            widget.borderRadius *
                                                (1 - _controller.value)),
                                        boxShadow: widget.boxShadow ??
                                            [
                                              BoxShadow(
                                                color: Colors.black
                                                    .withOpacity(0.5),
                                                blurRadius: 5,
                                              )
                                            ]),
                                    child: widget.borderRadius != 0
                                        ? ClipRRect(
                                            borderRadius: BorderRadius.circular(
                                                (1 - _controller.value) *
                                                    widget.borderRadius),
                                            child: scaffoldChild)
                                        : scaffoldChild);

                                if (_scaleFactor < 1)
                                  container = Transform.scale(
                                    alignment: _drawerInnerAlignment,
                                    scale: ((1 - _scaleFactor) *
                                            _controller.value) +
                                        _scaleFactor,
                                    child: container,
                                  );

                                // Vertical translate
                                if (widget.offset != null &&
                                    (widget.offset.top > 0 ||
                                        widget.offset.bottom > 0)) {
                                  final double translateY =
                                      MediaQuery.of(context).size.height *
                                          (widget.offset.top > 0
                                              ? -widget.offset.top
                                              : widget.offset.bottom);
                                  container = Transform.translate(
                                    offset: Offset(0,
                                        translateY * (1 - _controller.value)),
                                    child: container,
                                  );
                                }

                                return container;
                              },
                            )),
                      ),

                      ///Trigger
                      _trigger(
                          AlignmentDirectional.centerStart, widget.leftChild),
                      _trigger(
                          AlignmentDirectional.centerEnd, widget.rightChild),
                    ].where((a) => a != null).toList(),
                  ),
                ),
              ),
            ],
          ),
        );
      },
      child: widget.scaffold,
    );
  }
}

///An immutable set of offset in each of the four cardinal directions.
class IDOffset {
  const IDOffset.horizontal(
    double horizontal,
  )   : left = horizontal,
        top = 0.0,
        right = horizontal,
        bottom = 0.0;

  const IDOffset.only({
    this.left = 0.0,
    this.top = 0.0,
    this.right = 0.0,
    this.bottom = 0.0,
  })  : assert(top >= 0.0 &&
            top <= 1.0 &&
            left >= 0.0 &&
            left <= 1.0 &&
            right >= 0.0 &&
            right <= 1.0 &&
            bottom >= 0.0 &&
            bottom <= 1.0),
        assert(top >= 0.0 && bottom == 0.0 || top == 0.0 && bottom >= 0.0);

  /// The offset from the left.
  final double left;

  /// The offset from the top.
  final double top;

  /// The offset from the right.
  final double right;

  /// The offset from the bottom.
  final double bottom;
}

@Dn-a
Copy link
Owner

Dn-a commented Jul 14, 2020

@virskor thanks for the suggestion. Unfortunately, I don't have much time to run any tests at the moment. I'll let you know very soon

@cyrilcolinet
Copy link

cyrilcolinet commented Jul 15, 2020

@virskor Thanks for this quick fix.
@Dn-a If you need to test, I can help you if you need to.

Thanks again, you're the boss.

@virskor
Copy link

virskor commented Jul 15, 2020

@virskor Thanks for this quick fix.

@Dn-a If you need to test, I can help you if you need to.

Thanks again, you're the boss.

I am using this package and feel happy to share with you.😎

@FrankDupree
Copy link

I think it works, just the other way round.
according to this #47

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants