Home > Mobile >  How to prevent widget from passing out of screen border
How to prevent widget from passing out of screen border

Time:09-27

i am animating widget by Transform.translate like following

late Offset offsetAll =  const Offset(0,0);
              Transform.translate(
                offset:  offsetAll,
                child: GestureDetector(
                  onVerticalDragUpdate: (t){
                    offsetAll =t.delta;
                    setState(() {});
                  },
                  child: Container(
                    height: 100,
                    padding: const EdgeInsets.all(10),
                    color: Colors.black54,
                  ),
                ),
              );

i am moving the Container vertically. but the problem is when i move the Container to top or bottom i noticed it could be hidden like following

enter image description here

How could i prevent that ? .. how can i make it limit .. (if it arrive border so stop move )

i tried to wrap my widget into safeArea but does not work

CodePudding user response:

You can use CustomSingleChildLayout widget, which lets you position the child of this widget (the Container in your case) while giving you as input the size of the parent.

Why is this relevant? You ask. Well, you need to know the size of the child and the size of the parent in order to keep the child inside the parent bounds.

For example, if you are moving child to the right, then you want to stop moving at the moment you have: topLeftOfChildContainer.dx = Parent.size.width - child.width - paddingRight

If you want to have an idea how you do the calculations, see this method from the custom_positioned_widget class of the controllable_widgets package which uses CustomSingleChildLayout as explained above:

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // childSize: size of the content
    Offset childTopLeft = offsetBuilder.call(childSize);

    if (canGoOffParentBounds) {
      // no more checks on the position needed
      return childTopLeft;
    }

    // make sure the child does not go off screen in all directions
    // and respects the padding
    if (childTopLeft.dx   childSize.width > size.width - padding.right) {
      final distance =
          -(childTopLeft.dx - (size.width - padding.right - childSize.width));
      childTopLeft = childTopLeft.translate(distance, 0);
    }
    if (childTopLeft.dx < padding.left) {
      final distance = padding.left - childTopLeft.dx;
      childTopLeft = childTopLeft.translate(distance, 0);
    }
    if (childTopLeft.dy   childSize.height > size.height - padding.bottom) {
      final distance = -(childTopLeft.dy -
          (size.height - padding.bottom - childSize.height));
      childTopLeft = childTopLeft.translate(0, distance);
    }
    if (childTopLeft.dy < padding.top) {
      final distance = padding.top - childTopLeft.dy;
      childTopLeft = childTopLeft.translate(0, distance);
    }
    return childTopLeft;
  }

Full Working Example (without any package dependencies):

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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 const Exp3();
  }
}

typedef OffsetBuilder = Offset Function(Size size);

class Exp3 extends StatefulWidget {
  const Exp3({Key? key}) : super(key: key);

  @override
  State<Exp3> createState() => _Exp3State();
}

class _Exp3State extends State<Exp3> {
  // function that takes size of the child container and returns its new offset based on the size.
  // initial offset of the child container is (0, 0).
  OffsetBuilder _offsetBuilder = (_) => Offset.zero;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Builder(builder: (context) {
        return Container( // parent container
          color: Colors.red,
          child: GestureDetector(
            onPanUpdate: (details) {
              // get the current offset builder before we modify it
              // because we want to use it in the new offset builder
              final currentBuilder = _offsetBuilder;

              // create the new offset builder
              _offsetBuilder = (Size containerSize) {
                // the container size will be passed to you in this function
                // you can use it to place your widget
                // return the offset you like for the top left of the container
                // now we will return the current offset   the delta
                // Just be careful if you set canGoOffParentBounds to false, as this will prevent the widget from being painted outside the parent
                // but it WILL NOT prevent the offset from being updated to be outside parent, you should handle this in this case, see below:
                return currentBuilder.call(containerSize)   details.delta;
              };
              setState(() {}); // to update the UI (force rerender of the CustomSingleChildLayout)
            },
            child: CustomSingleChildLayout(
              delegate: MyCustomSingleChildLayoutDelegate(
                canGoOffParentBounds: false,
                padding: const EdgeInsets.all(8.0),
                offsetBuilder: _offsetBuilder,
              ),
              child: Container(
                width: 100,
                height: 100,
                color: Colors.yellow,
              ),
            ),
          ),
        );
      }),
    );
  }
}

class MyCustomSingleChildLayoutDelegate extends SingleChildLayoutDelegate {
  final Offset Function(Size childSize) offsetBuilder;
  final EdgeInsets padding;
  final bool canGoOffParentBounds;

  MyCustomSingleChildLayoutDelegate({
    required this.offsetBuilder,
    required this.padding,
    required this.canGoOffParentBounds,
  });

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    // The content can be at most the size of the parent minus 8.0 pixels in each
    // direction.
    return BoxConstraints.loose(constraints.biggest).deflate(padding);
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // childSize: size of the content
    Offset childTopLeft = offsetBuilder.call(childSize);

    if (canGoOffParentBounds) {
      // no more checks on the position needed
      return childTopLeft;
    }

    // make sure the child does not go off screen in all directions
    // and respects the padding
    if (childTopLeft.dx   childSize.width > size.width - padding.right) {
      final distance = -(childTopLeft.dx - (size.width - padding.right - childSize.width));
      childTopLeft = childTopLeft.translate(distance, 0);
    }
    if (childTopLeft.dx < padding.left) {
      final distance = padding.left - childTopLeft.dx;
      childTopLeft = childTopLeft.translate(distance, 0);
    }
    if (childTopLeft.dy   childSize.height > size.height - padding.bottom) {
      final distance = -(childTopLeft.dy - (size.height - padding.bottom - childSize.height));
      childTopLeft = childTopLeft.translate(0, distance);
    }
    if (childTopLeft.dy < padding.top) {
      final distance = padding.top - childTopLeft.dy;
      childTopLeft = childTopLeft.translate(0, distance);
    }
    return childTopLeft;
  }

  @override
  bool shouldRelayout(MyCustomSingleChildLayoutDelegate oldDelegate) {
    return oldDelegate.offsetBuilder != offsetBuilder;
  }
}

Note: Please note the comment that tells you that you should not update the offsetBuilder if by updating it, the child becomes outside parent bounds, because although the CustomSingleChildLayout will still paint the child inside the parent, but if you update the offsetBuilder anyway inside your stateful widget's state, you will have inconsistent state between the actual rendered container and the offsetBuilder of your state. So you should also check if child is still inside bounds inside the offsetBuilder.

And if you want you can use CustomPositionedWidget of the mentioned package directly.

p.s.: I am the maintainer of the package above.

  • Related