6

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:

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.

Kaushik NP
  • 6,733
  • 9
  • 31
  • 60
  • Linking this [issue](https://stackoverflow.com/questions/15398643/relativelayout-not-properly-updating-width-of-custom-view) here because it's related. Problem is actually on the parent `RelativeLayout` which does not honour (for some reason) our computed `width & height`. Try changing it to `LinearLayout` or `FrameLayout` it should work. Im still investigating on it but don't have time right now. – GDA Sep 12 '17 at 09:41

4 Answers4

9

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>
Community
  • 1
  • 1
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • Sorry I did not mention this previously (Have updated the question). I din't want to re-render the Custom Layout once its drawn on the screen. So require some method like `onMeasure` which calculates the dimensions before rendering it. And also should be able to be extended for future purposes. – Kaushik NP Sep 08 '17 at 14:26
  • @KaushikNP The method involving `onPreDraw()` will calculate the bounds before rendering the image. Nothing will flicker or appear to be redrawn on the screen. Try it. I think it will do what you want if I understanding your requirement. – Cheticamp Sep 08 '17 at 14:49
  • sorry didn't notice previously. The `onPreDraw()` was exactly what I needed!!! Thanks a ton! Accepting your answer. – Kaushik NP Sep 08 '17 at 15:20
  • Just one trivial problem (that I can survive but would like to solve if possible), is that the `Preview` option in Android studio for the xml does not render a Square (though it works properly in the app). The following link is a screenshot of that : [https://imgur.com/a/C92Zo](https://imgur.com/a/C92Zo) . I suppose you can't do anything about this, but why did it work properly when I was using `onMeasure` method if its due to its dynamic nature? – Kaushik NP Sep 08 '17 at 15:22
  • @KaushikNP I noticed that and almost mentioned it in my post, but I thought that it might just be related to the beta release I am using. I don't know how to handle this issue. The designer is pretty good, but it is not perfect. I have confirmed that `onPreDraw()` does run in the designer environment, so it may have something to do with how layout params and `requestLayout()` are handled. For the studio environment you could try `tools:layout_width="xdp"` and `tools:layout_height="xdp"` to force a square layut. This change will not effect the run environment. – Cheticamp Sep 08 '17 at 16:21
  • Not what I would really want. :/ I would love to have preview working. Is very useful in most cases. – Kaushik NP Sep 09 '17 at 14:45
  • 1
    @KaushikNP I took another look at getting the preview to work. The usual method of calling `requestLayout()` does not seem to work in edit mode. I don't know how to get the preview to work properly. It may be worth posing another question to specifically address that issue. If anything else occurs to me, I will post it here. – Cheticamp Sep 09 '17 at 16:24
  • Yes, I guess I would at that. Will wait for a few days, may find something meanwhile. **Note of interest**: I used `isInEditMode()` to try changing the measurements in `Preview mode`. Maybe the answer lies somewhere along those lines. – Kaushik NP Sep 09 '17 at 17:11
  • @KaushikNP I looked at `isInEditMode()`, but it is unclear how that will help if `requestLayout()` won't work. – Cheticamp Sep 09 '17 at 17:12
  • @KaushikNP Did you ever figure out the issue with the preview mode? I was hoping that someone would post a solution. – Cheticamp Sep 14 '17 at 14:32
  • Nope. To slightly improve the working for now, have put the old code in `isInEditMode()` , so it shows square layout in preview in few normal cases for now. Is a small hack until I can figure out another method to improve the preview or for creating square layout with the preview intact. – Kaushik NP Sep 14 '17 at 16:13
4

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

Ben P.
  • 52,661
  • 6
  • 95
  • 123
  • Agree that `ConstraingLayout` is a better solution. If he can change the layout to use it, it is better, future-proof. – Xavier Rubio Jansana Sep 05 '17 at 14:47
  • Yes, I do know about `ConstraintLayout`, and yes, I agree its a better method and something on which Google is putting a lot of stress on. But I still want to know how I can make this work with other layouts. – Kaushik NP Sep 05 '17 at 14:59
  • And a small note here, the 1st screenshot is not really a Square. Thanks for pointing out the Measured width and actual width. That might explain what's going on. – Kaushik NP Sep 05 '17 at 15:01
  • @KaushikNP You mean it's being clipped, don't you? – Xavier Rubio Jansana Sep 05 '17 at 15:04
  • 1
    @XavierRubioJansana, yes – Kaushik NP Sep 05 '17 at 15:05
  • @KaushikNP the first layout is a square that's being clipped by the bottom of the screen (since the square is defined by the screen's width). If you wanted instead to have a square that was defined by the remaining height, you could change the `layout_width` to `0dp` and add `app:layout_constraintBottom_toBottomOf="parent"` – Ben P. Sep 05 '17 at 15:06
  • @BenP. , again the requirement for it being extended from other layouts too. – Kaushik NP Sep 08 '17 at 14:28
  • @KaushikNP this solution works for any view type at all. The **parent** must be a `ConstraintLayout`, not the actual view you're trying to make square. – Ben P. Sep 08 '17 at 15:33
  • Ok, but I wanted a more Generic solution. Not just having Constraint Layout as parent. – Kaushik NP Sep 08 '17 at 15:35
1

You cannot compare the two measure specs, as they are not simply a size. You can see a very good explanation in this answer. This answer is for a custom view, but measure specs are the same. You need to get the mode and the size to compute final sizes, and compare the end results for both dimensions.

In the second example you shared, the right question is this one (third answer). Is written for Xamarin in C#, but is easy to understand.

The case that is failing for you is because you're finding an AT_MOST mode (when the view is hitting the bottom of the screen), that's why comparisons are failing in this case.

That should be the final method (can contain typos, I have been unable to test it:

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

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width, height;

    switch (widthMode) {
        case MeasureSpec.EXACTLY:
            width = widthSize;
            break;
        case MeasureSpec.AT_MOST:
            width = Math.min(widthSize, heightSize);
            break;
        default:
            width = 100;
            break;
    }

    switch (heightMode) {
        case MeasureSpec.EXACTLY:
            height = heightSize;
            break;
        case MeasureSpec.AT_MOST:
            height = Math.min(widthSize, heightSize);
            break;
        default:
            height = 100;
            break;
    }

    var size = Math.min(width, height);
    var newMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
    super.onMeasure(newMeasureSpec, newMeasureSpec);
}

I expect the end result to be roughly like this (maybe centered, but this dimensions):

final layout mock

Notice that this is a made up image done with Gimp.

Xavier Rubio Jansana
  • 6,388
  • 1
  • 27
  • 50
  • I tried out the code. Didn't work. Is the same as before. Going through the link you posted. Might find something useful there. – Kaushik NP Sep 05 '17 at 15:11
  • Two things: is exactly the same? Purple square should be smaller now, unless I'm missing something. Second, I misunderstood what you said, because of the comparison in your code. I thought you needed to make the squares **fit**, but you need them to **clip**, as @benp first screenshot, don't you? – Xavier Rubio Jansana Sep 05 '17 at 15:25
  • I need it to be a square. The Pink(outer square) should clip and its width should decrease too since it should to be a square. – Kaushik NP Sep 05 '17 at 15:27
  • I've added an image to roughly illustrate what I understand should be the end result. Is that correct? (Maybe centered, but that would be a next step) – Xavier Rubio Jansana Sep 05 '17 at 15:35
  • 1
    Timing buddy. So did I (image added in edit). And yes, its correct. I expect that. But what I get from the code is not that. – Kaushik NP Sep 05 '17 at 15:37
-1

try this. You can use on measure method to make a custom view. Check the link below for more details.

http://codecops.blogspot.in/2017/06/how-to-make-responsive-imageview-in.html

parambir singh
  • 224
  • 2
  • 13