I am writing up a custom layout (which extends FrameLayout) that can be zoomed in. All its children are also custom views which would actually get the scale factor from their parent via a getter method and scale accordingly by setting the scaled dimensions like
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
float scaleFactor = ((CustomLayout) getParent()).getCurrentScale();
setMeasuredDimension((int) (getMeasuredWidth() * scaleFactor), (int) (getMeasuredHeight() * scaleFactor));
}
I am using a ScaleGestureDetector to detect the "pinch to zoom" gesture and change the layout's scaleFactor. Then I force a layout on the custom layout by calling requestLayout. Unfortunately this doesn't seem to have any effect on its children. The children's onMeasure & onLayout are never called even though the parent goes through its measure & layout cycle. But, if I directly call requestLayout on one of the children, just that child gets scaled according to the scale factor set in the parent!!
It seems that unless requestLayout is exclusively called on a view, it doesn't actually measure itself again and instead uses some kind of cache. This is evident from the source code for view which says
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
How do I force the children also to go measure themselves again on calling requestLayout on their parent?
This will force relayout the view's children (given that view's own width and height does not need to change)
private static void relayoutChildren(View view) {
view.measure(
View.MeasureSpec.makeMeasureSpec(view.getMeasuredWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(view.getMeasuredHeight(), View.MeasureSpec.EXACTLY));
view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
}
Related
I have made class called ProgressButton that extended RelativeLayout.Now in main xml i added this class:
<com.tazik.progressbutton.ProgressButton
android:id="#+id/pb_button"
android:layout_width="200dp"
android:layout_height="wrap_content"/>
As you can see i added android:layout_width="200dp", now in ProgressButton class i want to get this size to create a button with this size:
public class ProgressButton extends RelativeLayout {
private AppCompatButton button;
public ProgressButton(Context context) {
super(context);
initView();
}
private void initView() {
initButton();
}
private void initButton() {
button = new AppCompatButton(getContext());
LayoutParams button_params = new LayoutParams(????, ViewGroup.LayoutParams.WRAP_CONTENT);
button_params.addRule(RelativeLayout.CENTER_IN_PARENT,RelativeLayout.TRUE);
button.setLayoutParams(button_params);
button.setText("click");
addView(button);
}
I want to create button exactly to size of relativeLayout, so how can i get layout_width in my custom view to set button_params width?
now in ProgressButton class i want to get this size to create a button with this size
As #MikeM. suggested in a comment. It could be as easy as giving that child view a width of MATCH_PARENT. See below...
LayoutParams button_params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
With that in place you don't need to worry about the actual size because MATCH_PARENT will stretch your child view to occupy the whole parent's width...obviosuly respecting margins and paddings.
However, if you do need to know the parent's width, you should query that in onMeasure. I strongly suggest you to stay away from onMeasure whenever possible because it is a bit complex and it might take a lot of your development time.
Either way, in onMeasure you can know what measurements the parent view wants to give to its child views, this is based on the space available to render inside the parent and the layout params specified...
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int childWidth = 0;
if(widthSpecMode == MeasureSpec.AT_MOST){
//The parent doesn't want the child to exceed "childWidth", it doesn't care if it smaller than that, just not bigger/wider
childWidth = MeasureSpec.getSize(widthMeasureSpec);
}
else if(widthSpecMode == MeasureSpec.EXACTLY){
//The parent wants the child to be exactly "childWidth"
childWidth = MeasureSpec.getSize(widthMeasureSpec);
}
else {
//The parent doesn't know yet what its children's width will be, probably
//because it's still taking measurements
}
//IMPORTANT!!! set your desired measurements (width and height) or call the base class's onMeasure method. Do one or the other, NOT BOTH
setMeasuredDimension(dimens, dimens);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Add a few Log.d calls inside onMeasure for a better understanding of what's happening. Be aware that this method will be called multiple times.
Again, this is an unnecessary overkill for your case scenario. Setting MATCH_PARENT to the button should produce the results you want
The height of RecyclerView item is not fixed,i need to set the background image for every item,so I want to get the height of recyclerview's item to resize the Image,but the itemView.getHeight() always return 0 in onBindViewHolder.
I have try to search many questions or articles,but i still cant get a good soluation.
Short
Measure the View manually
view.measure(
View.MeasureSpec.makeMeasureSpec(recyclerViewWidth, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
and get the height with view.getMeasuredHeight()
Background
A view will return a valid value for View.getHeight()only after it has been measured. The measuring itself will automatically happen by the system when the view is about to be displayed on screen.
When Android wants to display the layout, it will recursively call the view.layout() function for each view in the view tree. Each Parent tells its children the constraints they might have (width/height) and ask them to view.measure() themselves. As a result, the view will store the measured values BASED on the constraints in designated members (MeasuredHeight/Width). Note that at this point view.getMeasuredHeight() will hold the value while view.getHeight() will still be invalid. view.getHeight() will only return a valid value once the view has an actual height in the UI hierarchy.
Recap
So, to know the height of a view element, before it has been measured and laid out by the system, we will need to invoke the view.measure() function manually.
The measure function expects 2 parameters which derived from the view LayoutParams + the parent constraints.
In the above code sample, we are measuring the view forcing its width to be EXACTLY the width of the parent (the RecycleView), and the height is not limited.
I suggest that you define multiple layout files with the expected heights and inflate them according to some criteria in your data set.
ViewHolder onCreateViewHolder (ViewGroup parent, int viewType){
if(some condition){
//inflate layout 1
}else{
//inflate layout 2
}
or as answered here: you can get the measurements while initializing the view holder
itemView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int width = itemView.getMeasuredWidth();
int height = itemView.getMeasuredHeight();
How about this:
view.post(() -> {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
using this code to get recycler view's item height:
view.getViewTreeObserver().addOnGlobalLayoutListener(new
ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//don't forget remove this listener
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
//get item height here
int itemHeight = v.getHeight();
}
});
MyViewHolder.kt
With the addOnGlobalLayoutListener() method, the height value is obtained before the TextView is drawn. And then save it in a member variable.
The key is to modify the UI inside and outside the implementation of the listener so that there are no rendering problems (when the views are redrawn).
That is, you shoud use the getter inside the listener and; the setter inside and outside the listener.
companion object {
var maxHeight: Int = 0
fun create(mContext: Context): MyViewHolder {
val view = LayoutInflater.from(mContext).inflate(R.layout.item_answer, null)
updateLayout(view)
view.viewTreeObserver.addOnGlobalLayoutListener {
if (view.myTextView.height > maxHeight)
maxHeight = view.myTextView.height
updateLayout(view)
}
return MyViewHolder(mContext, view).apply {
setIsRecyclable(false)
}
}
fun updateLayout(view: View) {
if (maxHeight != 0 && view.myTextView.height != maxHeight)
view.myTextView.height = maxHeight
}
}
Source
I am currently overriding the onMeasure() method within a custom view in order to ensure it's width and height are equal and everithing appears to work except the parent linearlayout appears to think its dimensions haven't changed. I am currently using the setMeasuredDimension() method within the onMeasure override, in order to force the width to the measured height. Is there something else I need to do in order for the parent viewgroupto see these changes. Please note that the view is drawn at the correct size, but the parent view group now extends past the screen (it's not seeing the new width set from within the onMeasure() override).
As requested, I have pasted my onMeasure() code below:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int measuredHeight=getMeasuredHeight();
setMeasuredDimension(measuredHeight,measuredHeight);
if(measuredHeight > m_iconType.largestDimension()){
m_iconType.largestDimension(measuredHeight);
try {
Class<?>clazz = Class.forName(m_iconType.iconClassName());
Constructor<?> constructor = clazz.getConstructor(Integer.TYPE);
m_iconType.icon((CachedBoardElement)constructor.newInstance(measuredHeight));
} catch(Exception e){
e.printStackTrace();
}
}
}
Just to clarify, CachedBoardElement is a bitmap cached vector canvas whose maximum dimension is set during construction; it has to be recreated if view exceeds the current maximum.
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.
How do I compute the width and height of an inflated View if the parent is a PopupWindow, not a ViewGroup? I cannot use LayoutInflator.inflate(int resId, ViewGroup parent, attachToRoot boolean) because PopupWindow is not a ViewGroup, so I use LayoutInflator.inflate(int resId) instead, but after that I getWidth() and getHeight() return zero :(
I need to resize the PopupWindow to fit the View, but cannot do so until the View has a parent. Do I have a chicken-and-egg problem?
By the way the View is a subclass of RelativeView so calculating it manually is essentially out of the question.
Thanks in advance,
Barry
Actually popupWindow supports "wrap content" constans, so if you want popup be exact as your view - use this:
popup = new PopupWindow(context);
popup.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
--other options--
getWidth() and getHeight() returns zero, because view have size only after drawing on the screen. You can try to get values from LayoutParams of inflated view.
If there is fill_parent value - you are in egg-chicken situation.
If there is values in px or dp you can manually calculate size in pixels:
Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpSize, context.getResources().getDisplayMetrics()));
If there is wrap_content value - you can use view.measure() method - all children and view itself will be measured, and you can get view.getMeasuredHeight() and view.getMeasuredWidth().
As Jin35 said, your problem was that the view's width and height haven't been calculated yet...the dimensions aren't calculated until after the layout pass.
Using a fixed width and height from dimension resources (e.g. from a values/dimens.xml file) is one workaround, since then you don't need to wait for the view's onMeasure to occur -- you can get the value of the same dimension resource you used for the view you're interested in, and use that.
A better solution is to just delay your calculations until after the onMeasure happens. You can do that by overriding onMeasure, but a more elegant solution is to use a temporary OnGlobalLayoutListener like this:
View popup = LayoutInflator.inflate(int resId);
if(popup != null) {
// set up an observer that will be called once the listView's layout is ready
android.view.ViewTreeObserver viewTreeObserver = listView.getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// This will be called once the layout is finished, prior to displaying.
View popup = findViewById(resId);
if(popup != null) {
int width = popup.getMeasuredWidth();
int height = popup.getMeasuredHeight();
// don't need the listener any more
popup.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
});
}
}
Please try this:
Display display = getWindowManager().getDefaultDisplay();
Log.e("", "" + display.getHeight() + " " + display.getWidth());