Thursday, September 14, 2017

Custom Square Layout not working as expected

Leave a Comment

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.

4 Answers

Answers 1

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> 

Answers 2

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

Answers 3

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.

Answers 4

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

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment