6

I have a SliverPersistentHeader which contains a video. The desired behavior of this view is that as a user scrolls upward, the view should cover or minimize the size of the video. The video header is a widget containing a Chewie video player. The desired behavior works up to a certain point at which I get a pixel overflow as shown in this animation:

enter image description here

When the scroll reaches a certain point, the video can no longer resize and it results in a render overflow. The desired behavior would be for the video to continue to resize until it's gone, or to catch the error and hide or remove the video from the view. The code rendering this scroll view is:

  Widget buildScollView(GenericScreenModel model) {
    return CustomScrollView(
      slivers: [
        StandardHeader(),
        SliverFillRemaining(
          child: Container(
            // color: Colors.transparent,
              decoration: BoxDecoration(
                  border: Border.all(
                    color: Colors.white,
                  ),
                  borderRadius: BorderRadius.only(topRight: radius, topLeft: radius)),
              child: Padding(
                padding: const EdgeInsets.all(20.0),
                child: Text(model.model?.getContentText ?? 'Empty'),
              )),
        )
      ],
    );
  }

The StandardHeader class is a simple widget containing a Chewie video.

class _StandardHeaderState extends State<StandardHeader> {
  @override
  Widget build(BuildContext context) {
    return SliverPersistentHeader(
      floating: true,
      delegate: Delegate(
        Colors.blue,
        'Header Title',
      ),
      pinned: true,
    );
  }
}

Is there a way to catch this error and hide the video player? Can anyone help with this or point me to a resource? Thanks!

3
  • Did you ever figure this out? I'm running into the same thing. Commented Oct 10, 2022 at 5:26
  • Use LayoutBuilder inside Delegate's build functiuon to handle the height insufficient case? Commented Nov 18, 2022 at 6:55
  • @bryhaw see my answer for a solution. Commented Nov 18, 2022 at 16:00

1 Answer 1

1
+50

The issue seems to be with the Chewie and/or video player widget. If the header's height is less than the required height of the player, the overflow occurs.

You can achieve the desired effect by using a SingleChildRenderObjectWidget. I added an opacity factor that you can easily remove that gives it (in my opinion) an extra touch.

I named this widget: ClipBelowHeight

Output:

render_output.gif

Source:

ClipBelowHeight is SingleChildRenderObjectWidget that adds the desired effect by using a clipHeight parameter to clamp the height of the child to one that does not overflow. It centers its child vertically (Chewie player in this case).

To understand more, read the comments inside the performLayout and paint method.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class ClipBelowHeight extends SingleChildRenderObjectWidget {
  const ClipBelowHeight({
    super.key,
    super.child,
    required this.clipHeight,
    required this.opacityFactor,
  });

  /// The minimum height the [child] must have, as well as the height at which
  /// clipping begins.
  final double clipHeight;

  /// The opacity factor to apply when the height decreases.
  final double opacityFactor;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderClipBelowHeight(clipHeight: clipHeight, factor: opacityFactor);
  }

  @override
  void updateRenderObject(
    BuildContext context,
    RenderClipBelowHeight renderObject,
  ) {
    renderObject
      ..clipHeight = clipHeight
      ..factor = opacityFactor;
  }
}

class RenderClipBelowHeight extends RenderBox with RenderObjectWithChildMixin {
  RenderClipBelowHeight({required double clipHeight, required double factor})
      : _clipHeight = clipHeight,
        _factor = factor;

  double _clipHeight;
  double get clipHeight => _clipHeight;
  set clipHeight(double value) {
    assert(value >= .0);
    if (_clipHeight == value) return;
    _clipHeight = value;
    markNeedsLayout();
  }

  double _factor;
  double get factor => _factor;
  set factor(double value) {
    assert(value >= .0);
    if (_factor == value) return;
    _factor = value;
    markNeedsLayout();
  }

  @override
  bool get sizedByParent => false;

  @override
  void performLayout() {
    /// The child contraints depend on whether [constraints.maxHeight] is less
    /// than [clipHeight]. This RenderObject's responsibility is to ensure that
    /// the child's height is never below [clipHeight], because when the
    /// child's height is below [clipHeight], then there will be visual
    /// overflow.
    final childConstraints = constraints.maxHeight < _clipHeight
        ? BoxConstraints.tight(Size(constraints.maxWidth, _clipHeight))
        : constraints;

    (child as RenderBox).layout(childConstraints, parentUsesSize: true);

    size = Size(constraints.maxWidth, constraints.maxHeight);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final theChild = child as RenderBox;

    /// Clip the painted area to [size], which allows the [child] height to
    /// be greater than [size] without overflowing.
    context.pushClipRect(
      true,
      offset,
      Offset.zero & size,
      (PaintingContext context, Offset offset) {
        /// (optional) Set the opacity by applying the specified factor.
        context.pushOpacity(
          offset,
          /// The opacity begins to take effect at approximately half [size].
          ((255.0 + 128.0) * _factor).toInt(),
          (context, offset) {
            /// Ensure the child remains centered vertically based on [size].
            final centeredOffset =
                Offset(.0, (size.height - theChild.size.height) / 2.0);

            context.paintChild(theChild, centeredOffset + offset);
          },
        );
      },
    );
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    final theChild = child as RenderBox;
    var childParentData = theChild.parentData as BoxParentData;

    final isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset transformed) {
        assert(transformed == position - childParentData.offset);
        return theChild.hitTest(result, position: transformed);
      },
    );

    return isHit;
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;

  @override
  double computeMinIntrinsicWidth(double height) =>
      (child as RenderBox).getMinIntrinsicWidth(height);

  @override
  double computeMaxIntrinsicWidth(double height) =>
      (child as RenderBox).getMaxIntrinsicWidth(height);

  @override
  double computeMinIntrinsicHeight(double width) =>
      (child as RenderBox).getMinIntrinsicHeight(width);

  @override
  double computeMaxIntrinsicHeight(double width) =>
      (child as RenderBox).getMaxIntrinsicHeight(width);
}

The widget that uses the ClipBelowHeight widget is your header delegate. This widget should be self-explanatory and I think that you will be able to understand it.

class Delegate extends SliverPersistentHeaderDelegate {
  Delegate(this.color, this.player);

  final Color color;
  final Chewie player;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return Container(
      color: color,
      child: ClipBelowHeight(
        clipHeight: 80.0,
        opacityFactor: 1.0 - shrinkOffset / maxExtent,
        child: player,
      ),
    );
  }

  @override
  double get maxExtent => 150.0;

  @override
  double get minExtent => .0;

  @override
  bool shouldRebuild(Delegate oldDelegate) {
    return color != oldDelegate.color || player != oldDelegate.player;
  }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.