I tried to do custom component. I extended View class and do some drawing in onDraw overrided method. Why I need to override onMeasure? If I didn't, everything seen to be right. May someone explain it? How should I write my onMeasure method? I've seen couple tutorials, but each one is a little bit different than the other. Sometimes they call super.onMeasure at the end, sometimes they use setMeasuredDimension and didn't call it. Where is a difference?
After all I want to use several exactly the same components. I added those components to my XML file, but I don't know how big they should be. I want to set its position and size later (why I need to set size in onMeasure if in onDraw when I draw it, is working as well) in custom component class. When exactly I need to do that?
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 situation). These constraints are packaged up into the MeasureSpec values that are passed into the method. Here is a rough correlation of the mode values:
EXACTLY means the layout_width or layout_height value was set to a specific value. You should probably make your view this size. This can also get triggered when match_parent is used, to set the size exactly to the parent view (this is layout dependent in the framework).
AT_MOST typically means the layout_width or layout_height value was set to match_parent or wrap_content where a maximum size is needed (this is layout dependent in the framework), and the size of the parent dimension is the value. You should not be any larger than this size.
UNSPECIFIED typically means the layout_width or layout_height value was set to wrap_content with no restrictions. You can be whatever size you would like. Some layouts also use this callback to figure out your desired size before determine what specs to actually pass you again in a second measure request.
The contract that exists with onMeasure() is that setMeasuredDimension() MUST be called at the end with the size you would like the view to be. This method is called by all the framework implementations, including the default implementation found in View, which is why it is safe to call super instead if that fits your use case.
Granted, because the framework does apply a default implementation, it may not be necessary for you to override this method, but you may see clipping in cases where the view space is smaller than your content if you do not, and if you lay out your custom view with wrap_content in both directions, your view may not show up at all because the framework doesn't know how large it is!
Generally, if you are overriding View and not another existing widget, it is probably a good idea to provide an implementation, even if it is as simple as something like this:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredWidth = 100;
int desiredHeight = 100;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
//Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
//Must be this size
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
width = Math.min(desiredWidth, widthSize);
} else {
//Be whatever you want
width = desiredWidth;
}
//Measure Height
if (heightMode == MeasureSpec.EXACTLY) {
//Must be this size
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
height = Math.min(desiredHeight, heightSize);
} else {
//Be whatever you want
height = desiredHeight;
}
//MUST CALL THIS
setMeasuredDimension(width, height);
}
actually, your answer is not complete as the values also depend on the wrapping container. In case of relative or linear layouts, the values behave like this:
EXACTLY match_parent is EXACTLY + size of the parent
AT_MOST wrap_content results in an AT_MOST MeasureSpec
UNSPECIFIED never triggered
In case of an horizontal scroll view, your code will work.
If you don't need to change something onMeasure - there's absolutely no need for you to override it.
Devunwired code (the selected and most voted answer here) is almost identical to what the SDK implementation already does for you (and I checked - it had done that since 2009).
You can check the onMeasure method here :
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
Overriding SDK code to be replaced with the exact same code makes no sense.
This official doc's piece that claims "the default onMeasure() will always set a size of 100x100" - is wrong.
Related
In the Android source code, there is this block representing when onMeasure shouldn't not be called, which means after the first measure, if the following condition is met, onMeasure is not called.
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
//my understanding: parent measure mode or height/width is changed
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
//my understanding: parent measure mode is Exactly
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
//my understanding: last current(child) view measurement is the same as parent measurement
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
In my understanding, widthMeasureSpec is the parent measure spec. According to the source code, if parent measure mode is Exact and its width and height are changed, the child doesn't need to re-measure.
That doesn't make sense to me, if child is match_parent, even if parent is exact, when its width or height changed, the child needs to match and change as well. Do I miss something?
I have an Android application that extensively uses PopupWindows. I've found that when the layouts of the contents of the PopupWindow use WRAP_CONTENT the dialog will only grow to a specific width before it begins truncating the content. I've traced this down to a config.xml dimension:
<dimen name="config_prefDialogWidth">580dp</dimen>
This dimension is used to create a maximum width in the measureHierarchy method of ViewRootImpl when determining the dialog size. The following code is used to access the value:
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
The config_prefDialogWidth seems to have values for different device configurations, for example, the one I listed is for sw600dp. The one for default devices has 320dp as a value.
It would seem that this dimension is tuned for portrait orientation. Since my app forces landscape orientation this width is too small.
How do I override the config_prefDialogWidth dimension?
What you need is to make the width of your PopupWindow(wrap_content) larger than config_prefDialogWidth.
So here is my solution.
Choose one child View from the contentView of your PopupWindow.
A TextView, for example.
Override its onMeasure, add View.MEASURED_STATE_TOO_SMALL if necessary:
TextView textView = new TextView(context) {
#Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Text breaks into at least 2 lines, maybe the width is too small
if (getLineCount() > 1) {
int measuredWidth = getMeasuredWidth();
final int desiredMaxWidth = (getResources().getDisplayMetrics().widthPixels >> 2) * 3;
// Compare with the desired max width you want, for example: 3/4 * screenWidth
if (measuredWidth < disiredMaxWidth) {
// Tell ViewRootImpl that the width used to measure is too small
measuredWidth |= View.MEASURED_STATE_TOO_SMALL;
// ViewRootImpl will remeasure contentView with screem width.
setMeasuredDimension(measuredWidth, getMeasuredHeight());
}
}
}
};
Generally speaking:
Override onMeasure of one child View
Check whether the measured width is too small
Ask for remeasurement if necessary (Using View.MEASURED_STATE_TOO_SMALL)
You cannot override nor change config_prefDialogWidth's value because this dimension is a internal resource. A workaround is you must create your own class. You can copy PopupWindows from its source code, and modify it.
Is there a method in Android to get the height of the entire 'ScrollView'?
I tried 'layout.getMeasuredHeight()' but it gives the height only of the visible region.
You want the height of the ScrollView's child. Try scrollView.getChildAt(0).getMeasuredHeight().
EDIT
You could possibly try having the View measure itself and give it a measurespec that lets it use as much space as it wants. Something like this:
int width = ... // get screen width;
int height = 65536; // arbitrarily large value
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST);
final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
view.onMeasure(widthMeasureSpec, heightMeasureSpec);
// now get the height
int measuredHeight = view.getMeasuredHeight();
Full disclosure: I don't know if this will work or what effect it may have in your app. There is a side-effect of calling this method, which is that the view's measured dimensions are set. If it's possible, you might try to create a separate, duplicate view and measure that one; or, after you do this measuring hack, call requestLayout() on the view to schedule another measure & layout of the view tree.
A ScrollView always has only one child, so using:
ScrollView.getChildAt(0).getHeight();
should work.
What's the correct way of overriding onMeasure()? I've seen various approaches. For example, Professional Android Development uses MeasureSpec to calculate the dimensions, then ends with a call to setMeasuredDimension(). For example:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
this.setMeasuredDimension(parentWidth/2, parentHeight);
}
On the other hand, as per this post, the "correct" way is to use MeasureSpec, call setMeasuredDimensions(), followed by a call to setLayoutParams(), and ending with a call to super.onMeasure(). For example:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
this.setMeasuredDimension(parentWidth/2, parentHeight);
this.setLayoutParams(new *ParentLayoutType*.LayoutParams(parentWidth/2,parentHeight));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
So which is the right way? Neither approach has worked 100% for me.
I guess really what I'm asking is does anyone know of a tutorial that explains onMeasure(), layout, dimensions of child views etc.?
The other solutions are not comprehensive. They may work in some cases, and are a good place to start, but they may are not guaranteed to work.
When onMeasure gets called you may or may not have the rights to change the size. The values that are passed to your onMeasure (widthMeasureSpec, heightMeasureSpec) contain information about what your child view is allowed to do. Currently there are three values:
MeasureSpec.UNSPECIFIED - You can be as big as you'd like
MeasureSpec.AT_MOST - As big as you want (up to the spec size), This is parentWidth in your example.
MeasureSpec.EXACTLY - No choice. Parent has chosen.
This is done so that Android can make multiple passes to find the right size for each item, see here for more details.
If you do not follow these rules, your approach is not guaranteed to work.
For example if you want to check if you're allowed to change the size at all you can do the following:
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
Using this information you will know whether you can modify the values as in your code. Or if you are required to do something different. A quick and easy way to resolve your desired size is to use one of the following methods:
int resolveSizeAndState (int size, int measureSpec, int childMeasuredState)
int resolveSize (int size, int measureSpec)
While the first is only available on Honeycomb, the second is available on all versions.
Note: You may find that resizeWidth or resizeHeight are always false. I found this to be the case if I was requesting MATCH_PARENT. I was able to fix this by requesting WRAP_CONTENT on my parent layout and then during the UNSPECIFIED phase requesting a size of Integer.MAX_VALUE. Doing so gives you the max size your parent allows on the next pass through onMeasure.
The documentation is the authority on this matter: http://developer.android.com/guide/topics/ui/how-android-draws.html and http://developer.android.com/guide/topics/ui/custom-components.html
To summarize: at the end of your overridden onMeasure method you should call setMeasuredDimension.
You should not call super.onMeasure after calling setMeasuredDimension, that will just erase whatever you set. In some situations you might want to call the super.onMeasure first and then modify the results by calling setMeasuredDimension.
Don't call setLayoutParams in onMeasure. Layout happens in a second pass after measuring.
I think it depends on the parent which you are overriding.
For example, if you are extending a ViewGroup (like FrameLayout), when you have measured the size, you should call like below
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
because you may want to ViewGroup to do rest work (do some stuffs on child view)
If you are extending a View (like ImageView), you can just call this.setMeasuredDimension(width, height);, because the parent class will just do something like you have done usually.
In a word, if you want some features your parent class offers free you should call super.onMeasure() (pass MeasureSpec.EXACTLY mode measure spec usually), otherwise call this.setMeasuredDimension(width, height); is enough.
If changing the views size inside of onMeasure all you need is the setMeasuredDimension call. If you are changing the size outside of onMeasure you need to call setLayoutParams. For instance changing the size of a text view when the text is changed.
Depends on the control you are using. The instructions in the documentation work for some controls (TextView, Button, ...), but not for others (LinearLayout, ...). The way that worked very well for me was to call the super once I am done. Based on the article in the below link.
http://humptydevelopers.blogspot.in/2013/05/android-view-overriding-onmeasure.html
here is how I solved the problem:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
....
setMeasuredDimension( measuredWidth, measuredHeight );
widthMeasureSpec = MeasureSpec.makeMeasureSpec( measuredWidth, MeasureSpec.EXACTLY );
heightMeasureSpec = MeasureSpec.makeMeasureSpec( measuredHeight, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Moreover it was necessary for the ViewPager component
I guess, setLayoutParams and recalculating the measurements is a workaround to resize the child views correctly, as this is usually done in the derived class's onMeasure.
However, this rarely works correct (for whatever reason...), better invoke measureChildren (when deriving a ViewGroup) or try something similar when necessary.
you can take this piece of code as an example of onMeasure()::
public class MyLayerLayout extends RelativeLayout {
public MyLayerLayout(Context context) {
super(context);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
int currentChildCount = getChildCount();
for (int i = 0; i < currentChildCount; i++) {
View currentChild = getChildAt(i);
//code to find information
int widthPercent = currentChildInfo.getWidth();
int heightPercent = currentChildInfo.getHeight();
//considering we will pass height & width as percentage
int myWidth = (int) Math.round(parentWidth * (widthPercent / 100.0));
int myHeight = (int) Math.round(parentHeight * (heightPercent / 100.0));
//Considering we need to set horizontal & vertical position of the view in parent
AlignmentTraitValue vAlign = currentChildInfo.getVerticalLocation() != null ? currentChildlayerInfo.getVerticalLocation() : currentChildAlignmentTraitValue.TOP;
AlignmentTraitValue hAlign = currentChildInfo.getHorizontalLocation() != null ? currentChildlayerInfo.getHorizontalLocation() : currentChildAlignmentTraitValue.LEFT;
int topPadding = 0;
int leftPadding = 0;
if (vAlign.equals(currentChildAlignmentTraitValue.CENTER)) {
topPadding = (parentHeight - myHeight) / 2;
} else if (vAlign.equals(currentChildAlignmentTraitValue.BOTTOM)) {
topPadding = parentHeight - myHeight;
}
if (hAlign.equals(currentChildAlignmentTraitValue.CENTER)) {
leftPadding = (parentWidth - myWidth) / 2;
} else if (hAlign.equals(currentChildAlignmentTraitValue.RIGHT)) {
leftPadding = parentWidth - myWidth;
}
LayoutParams myLayoutParams = new LayoutParams(myWidth, myHeight);
currentChildLayoutParams.setMargins(leftPadding, topPadding, 0, 0);
currentChild.setLayoutParams(myLayoutParams);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
I wanted to create a custom LinearLayout (and later a custom ImageButton) that could take percentage values for both dimensions of size based on its parent's size regardless of the parent type (Relative or Linear). I was following this post: How to size an Android view based on its parent's dimensions, and it was very helpful, but I have a problem that those answers don't address.
When I place my Custom LinearLayout inside another LinearLayout, everything works as expected. My Custom LinearLayout covers the expected space (80% of the parent's width in the example below).
However if I place it inside a RelativeLayout, my screen always shows empty, I am not sure why this happens.
Here is my class:
public class ButtonPanel extends LinearLayout {
public ButtonPanel(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int newWidth = (int) Math.ceil(parentWidth * 0.8);
this.setMeasuredDimension(newWidth, parentHeight);
this.setLayoutParams(new RelativeLayout.LayoutParams(newWidth,parentHeight));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
And here is my testing layout for the activity.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.android.tests.views.ButtonPanel
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#drawable/inner_panel"
android:gravity="center_horizontal"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true">
</com.android.tests.views.ButtonPanel>
</RelativeLayout>
In my activity all I do is set the Content View to the above layout.
(Incidentally, does anybody now how I could get the type of the parent dynamically for setting the new LayoutParameters? Above you'll see the parent type (RelativeLayout) hard-coded into the Custom View onMeasure function)
Thanks in advance for any help!
Is this exposed to be a problem?
this.setLayoutParams(new RelativeLayout.LayoutParams(newWidth,parentHeight)); // <-- a RelativeLayout params?
In the onMeasure function you could use something like this to know what class is the parent of the view.
this.getParent().getClass().getName()
This should also work
a instanceof B
or
B.class.isAssignableFrom(a.getClass())
When using "instanceof", you need to know the class of "B" at compile time. When using "isAssignableFrom" it can be dynamic and change during runtime.
If you are not compfortable with string comparison, you could also use enums.
Turns out my two inquiries in this post were more related than expected.
I realized that by setting my view's LayoutParams to a completely new instance, I was overwriting the layout positioning information needed by the Relative Layout to position my view.
By 'zeroing out' that information, my view has the right dimensions, but the layout doesn't know where to place it, so it simply doesn't.
The following code for the new onMeasure shows how just directly modifying the height and width of the LayoutParams already attached to my view I avoid both overwriting the layout position information and having to create new LayoutParams based on the parent's type.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
int specHeight = MeasureSpec.getSize(heightMeasureSpec);
int newWidth = (int) Math.ceil(parentWidth * 0.8);
int newHeight = (int) Math.ceil(parentHeight * 0.8);
this.setMeasuredDimension(newWidth, newHeight);
this.getLayoutParams().height = newHeight;
this.getLayoutParams().width = newWidth;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Now, I'll be honest and say that this code is still not bug-free. Bringing the activity to the foreground and background multiple times constantly reduces the size of this custom view. The 0.8 reduction factor gets applied over and over each time the activity is brought up (I suspect the setting of the LayoutParams has to do with it, it might actually be unnecessary, but I haven't has time to test).
BUT, this still answered the question concerning this post, namely, why was my view not appearing at all despite having the right dimensions.