Amir Ali Aghamali
Amir Ali Aghamali

Reputation: 642

Flutter overflowed positioned Button is not clickable

I have a stack widget parenting a Positioned widget like this:

Stack(
        overflow: Overflow.visible,
        children: [
          Container(
            width: 150,
            height: 150,
          ),
          Positioned(
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
                print('FAB tapped!');
              },
              backgroundColor: Colors.blueGrey,
            ),
            right: 0,
            left: 0,
            bottom: -26,
          ),
        ],
      ),

That part of the fab which is placed outside the container is not clickable, what is the solution? and here is a screenshot:

enter image description here

Upvotes: 27

Views: 19075

Answers (7)

Roger Gusmao
Roger Gusmao

Reputation: 4106

You have to put the button in the last place of the Stack's children

Stack(children: [...., buttonWidget ])

Upvotes: 4

Agung
Agung

Reputation: 13803

up until now, there is now solution from Flutter, I should make a simple trick to solve this issue, I need to make a layout like this

enter image description here

the workaround is by adding a SizedBox below your background widget, the height of the SizedBox should be the same as the height of the overlaping widget.

like this

           Stack(
              clipBehavior: Clip.none,
              children: [
                Column( // wrap the background in a column
                  children: [
                    const _HeaderBackground(),
                    SizedBox(height: 100), // add the SizedBox with height = 100.0
                  ],
                ),
                Positioned(
                  bottom: 16,
                  left: 4,
                  right: 4,
                  child: _ReferralCodeSection(customer), // the height  of this widget is 100
                ),
              ],
            ),

Upvotes: 4

Lanistor
Lanistor

Reputation: 303

Flutter does not officially plan to solve this problem, so we can only use some hacking methods.

Here is my resolution with an example, you can use the following OverflowWithHitTest Widget directlly:

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

/// Creates a widget that can check its' overflow children's hitTest
///
/// [overflowKeys] is must, and there should be used on overflow widget's outermost widget those' sizes cover the overflow child, because it will [hitTest] its' children, but not [hitTest] its' parents. And i cannot found a way to check RenderBox's parent in flutter.
///
/// The [OverflowWithHitTest]'s size must contains the overflow widgets, so you can use it as outer as possible.
///
/// This will not reduce rendering performance, because it only overcheck the given widgets marked by [overflowKeys].
///
/// Demo:
///
/// class _MyPageStore extends State<MyPage> {
///
///   var overflowKeys = <GlobalKey>[GlobalKey()];
///
///   Widget build(BuildContext context) {
///     return Scaffold(
///       body: OverflowWithHitTest(
///
///         overflowKeys: overflowKeys,
///
///         child: Container(
///           height: 50,
///           child: UnconstrainedBox(
///             child: Container(
///               width: 200,
///               height: 50,
///               color: Colors.red,
///               child: OverflowBox(
///                 alignment: Alignment.topLeft,
///                 minWidth: 100,
///                 maxWidth: 200,
///                 minHeight: 100,
///                 maxHeight: 200,
///                 child: GestureDetector(
///                   key: overflowKeys[0],
///                   behavior: HitTestBehavior.translucent,
///                   onTap: () {
///                     print('==== onTap;');
///                   },
///                   child: Container(
///                     color: Colors.blue,
///                     height: 200,
///                     child: Text('aaaa'),
///                   ),
///                 ),
///               ),
///             ),
///           ),
///         ),
///       ),
///     );
///   }
/// }
///
///
class OverflowWithHitTest extends SingleChildRenderObjectWidget {
  const OverflowWithHitTest({
    required this.overflowKeys,
    Widget? child,
    Key? key,
  }) : super(key: key, child: child);

  final List<GlobalKey> overflowKeys;

  @override
  _OverflowWithHitTestBox createRenderObject(BuildContext context) {
    return _OverflowWithHitTestBox(overflowKeys: overflowKeys);
  }

  @override
  void updateRenderObject(
      BuildContext context, _OverflowWithHitTestBox renderObject) {
    renderObject.overflowKeys = overflowKeys;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(
        DiagnosticsProperty<List<GlobalKey>>('overflowKeys', overflowKeys));
  }
}

class _OverflowWithHitTestBox extends RenderProxyBoxWithHitTestBehavior {
  _OverflowWithHitTestBox({required List<GlobalKey> overflowKeys})
      : _overflowKeys = overflowKeys,
        super(behavior: HitTestBehavior.translucent);

  /// Global keys of overflow children
  List<GlobalKey> get overflowKeys => _overflowKeys;
  List<GlobalKey> _overflowKeys;

  set overflowKeys(List<GlobalKey> value) {
    var changed = false;

    if (value.length != _overflowKeys.length) {
      changed = true;
    } else {
      for (var ind = 0; ind < value.length; ind++) {
        if (value[ind] != _overflowKeys[ind]) {
          changed = true;
        }
      }
    }
    if (!changed) {
      return;
    }
    _overflowKeys = value;
    markNeedsPaint();
  }

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (hitTestOverflowChildren(result, position: position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget =
          hitTestChildren(result, position: position) || hitTestSelf(position);
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
  }

  bool hitTestOverflowChildren(BoxHitTestResult result,
      {required Offset position}) {
    if (overflowKeys.length == 0) {
      return false;
    }
    var hitGlobalPosition = this.localToGlobal(position);
    for (var child in overflowKeys) {
      if (child.currentContext == null) {
        continue;
      }
      var renderObj = child.currentContext!.findRenderObject();
      if (renderObj == null || renderObj is! RenderBox) {
        continue;
      }

      var localPosition = renderObj.globalToLocal(hitGlobalPosition);
      if (renderObj.hitTest(result, position: localPosition)) {
        return true;
      }
    }
    return false;
  }
}

Upvotes: 2

Vahid Rajabi
Vahid Rajabi

Reputation: 359

The problem is when a child overflows on Stack that has Clip.none behavior, the part that is outside of Stack would not be recognized to be clicked.

Solution :

Wrap the Stack with Column and add the space you want to be outside of Stack :

final _clipSpace = 30;
Stack(
  clipBehavior: Clip.none,
  children: [
    Column(
      children: [
        DecoratedBox(
          decoration: const BoxDecoration(// decorate the box //
           ),
          child: Column(
            children: [
              // column's children
                ],
              )
            ],
          ),
        ),
         // clip space
        const SizedBox(height: _clipSpace,)
      ],
    ),
    const Positioned(
      child: _ActionButton(),
      left: 0,
      right: 0,
      bottom: 0,
    ),
  ],
);

Upvotes: 9

Ferran Buireu
Ferran Buireu

Reputation: 29320

Providing an updated answer since overflow specification is deprecated after v1.22.0-12.0.pre. clipBehavior is the replacing property:

  Stack(
    clipBehavior: Clip.none,
    children: [
      Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>
        [
          Container(width: 150, height: 150, color: Colors.yellow),
          Container(width: 150, height: 28, color: Colors.transparent),
        ],
      ),
      Positioned(
        child: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () {
            print('FAB tapped!');
          },
          backgroundColor: Colors.blueGrey,
        ),
        right: 0,
        left: 0,
        bottom: 0,
      ),
    ],
  )

Note: credits to @Amir's answer

Upvotes: 6

Jitesh Mohite
Jitesh Mohite

Reputation: 34210

Container(
        width: 150,
        height: 180,
        child: Stack(
          children: [
            Container(
                width: double.infinity,
                height: 150,
               child: Image.asset('assets/images/image.jpg', fit: BoxFit.cover,)
            ),
            Container(
              alignment: Alignment.bottomCenter,
              child: FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                  print('FAB tapped!');
                },
                backgroundColor: Colors.blueGrey,
              ),
            ),
          ],
        ),
      ),

Fab button is not clickable because it renders outside of stack as you have given -ve bottom, Ideally, you should have parent container and inside it has all stack widget you should render it. Here I have used hardcoded values, but you should use media query as per your requirement

Like:

Container(
        width: MediaQuery.of(context).size.width * 0.3,
        height: MediaQuery.of(context).size.height * 0.3,
        child: Stack(
          children: [
            Container(
                width: double.infinity,
                height: MediaQuery.of(context).size.height * 0.26,
               child: Image.asset('assets/images/jitesh.jpg', fit: BoxFit.cover,)
            ),
            Container(
              alignment: Alignment.bottomCenter,
              child: FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                  print('FAB tapped!');
                },
                backgroundColor: Colors.blueGrey,
              ),
            ),
          ],
        ),
      ),

Upvotes: 2

Amir
Amir

Reputation: 2355

try this :

      Stack(
        overflow: Overflow.visible,
        children: [
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>
            [
              Container(width: 150, height: 150, color: Colors.yellow),
              Container(width: 150, height: 28, color: Colors.transparent),
            ],
          ),
          Positioned(
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
                print('FAB tapped!');
              },
              backgroundColor: Colors.blueGrey,
            ),
            right: 0,
            left: 0,
            bottom: 0,
          ),
        ],
      )

you should keep button inside of stack if you want it to stay clickable

Upvotes: 18

Related Questions