Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Square Layout not working as expected

I stumbled across this problem when working with custom Square Layout : by extending the Layout and overriding its onMeasure() method to make the dimensions = smaller of the two (height or width).

Following is the custom Layout code :

public class CustomSquareLayout extends RelativeLayout{


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

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

    public CustomSquareLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CustomSquareLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //Width is smaller
        if(widthMeasureSpec < heightMeasureSpec)
            super.onMeasure(widthMeasureSpec, widthMeasureSpec);

            //Height is smaller
        else
            super.onMeasure(heightMeasureSpec, heightMeasureSpec);

    }
}

The custom Square Layout works fine, until in cases where the custom layout goes out of bound of the screen. What should have automatically adjusted to screen dimensions though, doesn't happen. As seen below, the CustomSquareLayout actually extends below the screen (invisible). What I expect is for the onMeasure to handle this, and give appropriate measurements. But that is not the case. Note of interest here is that even thought the CustomSquareLayout behaves weirdly, its child layouts all fall under a Square shaped layout that is always placed on the Left hand side.

enter image description here

<!-- XML for above image -->
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="300dp"
        android:text="Below is the Square Layout"
        android:gravity="center"
        android:id="@+id/text"
        />

    <com.app.application.CustomSquareLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/text"
        android:background="@color/colorAccent"             #PINK
        android:layout_centerInParent="true"
        android:id="@+id/square"
        android:padding="16dp"
        >
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"            #Note this
            android:background="@color/colorPrimaryDark"    #BLUE
            >
        </RelativeLayout>
    </com.app.application.CustomSquareLayout>

</RelativeLayout>

Normal case : (Textview is in Top)

enter image description here

Following are few links I referenced:

  • Custom Square LinearLayout. How?
  • Simple way to do dynamic but square layout

Hope to find a solution to this, using onMeasure or any other function when extending the layout (so that even if some extends the Custom Layout, the Square property remains)

Edit 1 : For further clarification, the expected result for 1st case is shownenter image description here

Edit 2 : I gave a preference to onMeasure() or such functions as the need is for the layout specs (dimensions) to be decided earlier (before rendering). Otherwise changing the dimensions after the component loads is simple, but is not requested.

like image 375
Kaushik NP Avatar asked Sep 05 '17 13:09

Kaushik NP


2 Answers

You can force a square view by checking for "squareness" after layout. Add the following code to onCreate().

final View squareView = findViewById(R.id.square);
squareView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        squareView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        if (squareView.getWidth() != squareView.getHeight()) {
            int squareSize = Math.min(squareView.getWidth(), squareView.getHeight());
            RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) squareView.getLayoutParams();
            lp.width = squareSize;
            lp.height = squareSize;
            squareView.requestLayout();
        }
    }
});

This will force a remeasurement and layout of the square view with a specified size that replaces MATCH_PARENT. Not incredibly elegant, but it works.

enter image description here

You can also add a PreDraw listener to your custom view.

onPreDraw

boolean onPreDraw ()

Callback method to be invoked when the view tree is about to be drawn. At this point, all views in the tree have been measured and given a frame. Clients can use this to adjust their scroll bounds or even to request a new layout before drawing occurs.

Return true to proceed with the current drawing pass, or false to cancel.

Add a call to an initialization method in each constructor in the custom view:

private void init() {
    this.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            if (getWidth() != getHeight()) {
                int squareSize = Math.min(getWidth(), getHeight());
                RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams();
                lp.width = squareSize;
                lp.height = squareSize;
                requestLayout();
                return false;
            }
            return true;
        }
    });
}

The XML can look like the following:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="300dp"
        android:gravity="center"
        android:text="Below is the Square Layout" />

    <com.example.squareview.CustomSquareLayout
        android:id="@+id/square"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/text"
        android:background="@color/colorAccent"
        android:padding="16dp">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:background="@color/colorPrimaryDark" />
    </com.example.squareview.CustomSquareLayout>

</RelativeLayout>
like image 126
Cheticamp Avatar answered Sep 28 '22 07:09

Cheticamp


There is a difference between the view's measured width and the view's width (same for height). onMeasure is only setting the view's measured dimensions. There is still a different part of the drawing process that constrains the view's actual dimensions so that they don't go outside the parent.

If I add this code:

    final View square = findViewById(R.id.square);
    square.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            System.out.println("measured width: " + square.getMeasuredWidth());
            System.out.println("measured height: " + square.getMeasuredHeight());
            System.out.println("actual width: " + square.getWidth());
            System.out.println("actual height: " + square.getHeight());
        }
    });

I see this in the logs:

09-05 10:19:25.768  4591  4591 I System.out:  measured width: 579
09-05 10:19:25.768  4591  4591 I System.out:  measured height: 579
09-05 10:19:25.768  4591  4591 I System.out:  actual width: 768
09-05 10:19:25.768  4591  4591 I System.out:  actual height: 579

How to solve it by creating a custom view? I don't know; I never learned. But I do know how to solve it without having to write any Java code at all: use ConstraintLayout.

ConstraintLayout supports the idea that children should be able to set their dimensions using an aspect ratio, so you can simply use a ratio of 1 and get a square child. Here's my updated layout (the key piece is the app:layout_constraintDimensionRatio attr):

<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="300dp"
        android:gravity="center"
        android:text="Below is the Square Layout"
        app:layout_constraintTop_toTopOf="parent"/>

    <RelativeLayout
        android:id="@+id/square"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:padding="16dp"
        android:background="@color/colorAccent"
        app:layout_constraintTop_toBottomOf="@+id/text"
        app:layout_constraintDimensionRatio="1">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:background="@color/colorPrimaryDark">
        </RelativeLayout>

    </RelativeLayout>

</android.support.constraint.ConstraintLayout>

And screenshots:

enter image description here enter image description here

like image 34
Ben P. Avatar answered Sep 28 '22 08:09

Ben P.