Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom view's onMeasure: how to get width based on height

Tags:

The previous version of my question was too wordy. People couldn't understand it, so the following is a complete rewrite. See the edit history if you are interested in the old version.

A RelativeLayout parent sends MeasureSpecs to its child view's onMeasure method in order to see how big the child would like to be. This occurs in several passes.

My custom view

I have a custom view. As the view's content increases, the view's height increases. When the view reaches the maximum height that the parent will allow, the view's width increases for any additional content (as long as wrap_content was selected for the width). Thus, the width of the custom view is directly dependant on what parent says the maximum hight must be.

enter image description here

An (inharmonious) parent child conversation

onMeasure pass 1

The RelativeLayout parent tells my view, "You can be any width up to 900 and any height up to 600. What do you say?"

My view says, "Well, at that height, I can fit everything with a width of 100. So I'll take a width of 100 and a height of 600."

onMeasure pass 2

The RelativeLayout parent tells my view, "You told me last time that you wanted a width of 100, so let's set that as an exact width. Now, based on that width, what kind of height would you like? Anything up to 500 is OK."

"Hey!" my view replies. "If you're only giving me a maximum hight of 500, then 100 is too narrow. I need a width of 200 for that height. But fine, have it your way. I won't break the rules (yet...). I'll take a width of 100 and a height of 500."

Final result

The RelativeLayout parent assigns the view a final size of 100 for the width and 500 for the height. This is of course too narrow for the view and part of the content gets clipped.

"Sigh," thinks my view. "Why won't my parent let me be wider? There is plenty of room. Maybe someone on Stack Overflow can give me some advice."

like image 923
Suragch Avatar asked Feb 22 '17 11:02

Suragch


People also ask

How do I get custom height view?

You can use the following method to get the width and height of the view, For example, int height = yourView. getLayoutParams(). height; int width = yourView.

What is onMeasure method in Android?

onMeasure() is your opportunity to tell Android how big you want your custom view to be dependent the layout constraints provided by the parent; it is also your custom view's opportunity to learn what those layout constraints are (in case you want to behave differently in a match_parent situation than a wrap_content ...

What is custom view in Android?

Custom Views is just a way to make an android developer a painter. When you need to create some custom and reuse the views when it is not provided by the Android Ecosystem. Custom Views can be used as widgets like TextView, EditText etc.


2 Answers

Update: Modified code to fix some things.

First, let me say that you asked a great question and laid out the problem very well (twice!) Here is my go at a solution:

It seems that there is a lot going on with onMeasure that, on the surface, doesn't make a lot of sense. Since that is the case, we will let onMeasure run as it will and at the end pass judgment on the View's bounds in onLayoutby setting mStickyWidth to the new minimum width we will accept. In onPreDraw, using a ViewTreeObserver.OnPreDrawListener, we will force another layout (requestLayout). From the documentation (emphasis added):

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.

The new minimum width set in onLayout will now be enforced by onMeasure which is now smarter about what is possible.

I have tested this with your example code and it seems to work OK. It will need much more testing. There may be other ways to do this, but that is the gist of the approach.

CustomView.java

import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver;  public class CustomView extends View         implements ViewTreeObserver.OnPreDrawListener {     private int mStickyWidth = STICKY_WIDTH_UNDEFINED;      public CustomView(Context context) {         super(context);     }      public CustomView(Context context, AttributeSet attrs) {         super(context, attrs);     }      @Override     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {         logMeasureSpecs(widthMeasureSpec, heightMeasureSpec);         int desiredHeight = 10000; // some value that is too high for the screen         int desiredWidth;          int widthMode = MeasureSpec.getMode(widthMeasureSpec);         int widthSize = MeasureSpec.getSize(widthMeasureSpec);         int heightMode = MeasureSpec.getMode(heightMeasureSpec);         int heightSize = MeasureSpec.getSize(heightMeasureSpec);          int width;         int height;          // Height         if (heightMode == MeasureSpec.EXACTLY) {             height = heightSize;         } else if (heightMode == MeasureSpec.AT_MOST) {             height = Math.min(desiredHeight, heightSize);         } else {             height = desiredHeight;         }          // Width         if (mStickyWidth != STICKY_WIDTH_UNDEFINED) {             // This is the second time through layout and we are trying renogitiate a greater             // width (mStickyWidth) without breaking the contract with the View.             desiredWidth = mStickyWidth;         } else if (height > BREAK_HEIGHT) { // a number between onMeasure's two final height requirements             desiredWidth = ARBITRARY_WIDTH_LESSER; // arbitrary number         } else {             desiredWidth = ARBITRARY_WIDTH_GREATER; // arbitrary number         }          if (widthMode == MeasureSpec.EXACTLY) {             width = widthSize;         } else if (widthMode == MeasureSpec.AT_MOST) {             width = Math.min(desiredWidth, widthSize);         } else {             width = desiredWidth;         }          Log.d(TAG, "setMeasuredDimension(" + width + ", " + height + ")");         setMeasuredDimension(width, height);     }      @Override     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {         int w = right - left;         int h = bottom - top;          super.onLayout(changed, left, top, right, bottom);         // Here we need to determine if the width has been unnecessarily constrained.         // We will try for a re-fit only once. If the sticky width is defined, we have         // already tried to re-fit once, so we are not going to have another go at it since it         // will (probably) have the same result.         if (h <= BREAK_HEIGHT && (w < ARBITRARY_WIDTH_GREATER)                 && (mStickyWidth == STICKY_WIDTH_UNDEFINED)) {             mStickyWidth = ARBITRARY_WIDTH_GREATER;             getViewTreeObserver().addOnPreDrawListener(this);         } else {             mStickyWidth = STICKY_WIDTH_UNDEFINED;         }         Log.d(TAG, ">>>>onLayout: w=" + w + " h=" + h + " mStickyWidth=" + mStickyWidth);     }      @Override     public boolean onPreDraw() {         getViewTreeObserver().removeOnPreDrawListener(this);         if (mStickyWidth == STICKY_WIDTH_UNDEFINED) { // Happy with the selected width.             return true;         }          Log.d(TAG, ">>>>onPreDraw() requesting new layout");         requestLayout();         return false;     }      protected void logMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) {         int widthMode = MeasureSpec.getMode(widthMeasureSpec);         int widthSize = MeasureSpec.getSize(widthMeasureSpec);         int heightMode = MeasureSpec.getMode(heightMeasureSpec);         int heightSize = MeasureSpec.getSize(heightMeasureSpec);         String measureSpecHeight;         String measureSpecWidth;          if (heightMode == MeasureSpec.EXACTLY) {             measureSpecHeight = "EXACTLY";         } else if (heightMode == MeasureSpec.AT_MOST) {             measureSpecHeight = "AT_MOST";         } else {             measureSpecHeight = "UNSPECIFIED";         }          if (widthMode == MeasureSpec.EXACTLY) {             measureSpecWidth = "EXACTLY";         } else if (widthMode == MeasureSpec.AT_MOST) {             measureSpecWidth = "AT_MOST";         } else {             measureSpecWidth = "UNSPECIFIED";         }          Log.d(TAG, "Width: " + measureSpecWidth + ", " + widthSize + " Height: "                 + measureSpecHeight + ", " + heightSize);     }      private static final String TAG = "CustomView";     private static final int STICKY_WIDTH_UNDEFINED = -1;     private static final int BREAK_HEIGHT = 1950;     private static final int ARBITRARY_WIDTH_LESSER = 200;     private static final int ARBITRARY_WIDTH_GREATER = 800; } 
like image 142
Cheticamp Avatar answered Sep 30 '22 18:09

Cheticamp


To make custom layout you need to read and understand this article https://developer.android.com/guide/topics/ui/how-android-draws.html

It isn't difficult to implement behaviour you want. You just need to override onMeasure and onLayout in your custom view.

In onMeasure you will get max possible height of your custom view and call measure() for childs in cycle. After child measurement get desired height from each child and calculate is child fit in current column or not, if not increase custom view wide for new column.

In onLayout you must call layout() for all child views to set them positions within the parent. This positions you have calculated in onMeasure before.

like image 20
Nik Avatar answered Sep 30 '22 17:09

Nik