Search code examples
androidlayout

Position View outside parent without being clipped


I have a JSON like this:

{
  "name": "view",
  "attributes": {
    "style": {
      "backgroundColor": "yellow",
    }
  },
  "children": [
    {
      "name": "view",
      "attributes": {
        "style": {
          "backgroundColor": "red",
          "width": 400,
          "height": 400,
        }
      }
    },
    {
      "name": "view",
      "attributes": {
        "style": {
          "backgroundColor": "yellow",
          "top": 150,
          "left": 100,
          "width": 200,
          "height": 200,
        }
      }
    }
  ]
}

I'm using recursion to display a layout based on that JSON.

My CustomLayout extends from ViewGroup. It's similar to LinearLayout.

public static class CustomLayout extends ViewGroup {

    private int orientation; // 0 for horizontal, 1 for vertical

    public CustomLayout(Context context) {
        super(context);
    }

    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // Set the orientation (horizontal or vertical)
    public void setOrientation(int orientation) {
        this.orientation = orientation;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Measure the size of your layout and its children here
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = 0;
        int height = 0;

        // Measure each child view
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            if (orientation == 0) { // Horizontal layout
                width += child.getMeasuredWidth();
                height = Math.max(height, child.getMeasuredHeight());
            } else { // Vertical layout
                width = Math.max(width, child.getMeasuredWidth());
                height += child.getMeasuredHeight();
            }
        }

        // Take padding into account
        width += getPaddingLeft() + getPaddingRight();
        height += getPaddingTop() + getPaddingBottom();

        // Respect the given measure spec
        width = resolveSizeAndState(width, widthMeasureSpec, 0);
        height = resolveSizeAndState(height, heightMeasureSpec, 0);

        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
                heightMode == MeasureSpec.EXACTLY ? heightSize : height);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // Position child views within your layout here
        int childLeft = getPaddingLeft();
        int childTop = getPaddingTop();

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (orientation == 0) { // Horizontal layout
                child.layout(childLeft, childTop,
                        childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
                childLeft += child.getMeasuredWidth();
            } else { // Vertical layout
                child.layout(childLeft, childTop,
                        childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
                childTop += child.getMeasuredHeight();
            }
        }
    }
}

The recursion ends up with something like this under the hood:

LinearLayout rootView = activity.findViewById(R.id.root);

CustomLayout view = new CustomLayout(activity);

view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));

view.setOrientation(1);

view.setBackgroundColor(Color.parseColor("yellow"));

View child1 = new CustomLayout(activity);
View child2 = new CustomLayout(activity);

child1.setLayoutParams(new LayoutParams(400, 400));
child1.setBackgroundColor(Color.parseColor("red"));

child2.setLayoutParams(new LayoutParams(200, 200));
child2.setBackgroundColor(Color.parseColor("green"));

view.addView(child1);
view.addView(child2);

rootView.addView(view);

My question is, how to apply top and left?

This is what I want to achieve:

made with react native

The desired result is made with React Native. This is the code:

function App(): React.JSX.Element {
  return (
    <View style={{backgroundColor: 'yellow'}}>
      <View style={{backgroundColor: 'red', width: 200, height: 200}}></View>
      <View
        style={{
          backgroundColor: 'green',
          top: 100,
          left: 50,
          width: 150,
          height: 150,
        }}></View>
    </View>
  );
}

As you can see, the yellow wrapper gets the right width and height based on its children, and the green view is moved outside of the wrapper without being clipped.

React Native uses its own layout extending ViewGroup also, so there must be a way.


Solution

  • The solution is to add these lines to every parent:

    setClipChildren(false);
    setClipToPadding(false);
    

    Probably something like this:

    public CustomLayout(Context context) {
        super(context);
    
        setClipChildren(false);
        setClipToPadding(false);
    }
    

    And inside onLayout we have something like this for the children:

    int x = 30; // 30 to the left
    int y = 0;
    
    int childWidth = child.getMeasuredWidth();
    int childHeight = child.getMeasuredHeight();
    
    child.layout(x, y, x + childWidth, y + childHeight);