108

How can I size a view based on the size of its parent layout. For example I have a RelativeLayout that fills the full screen, and I want a child view, say an ImageView, to take up the whole height, and 1/2 the width?

I've tried overriding all on onMeasure, onLayout, onSizeChanged, etc and I couldn't get it to work....

Mike Baxter
  • 6,868
  • 17
  • 67
  • 115
Mitch
  • 1,105
  • 2
  • 8
  • 3
  • It is possible to do that using ConstraintLayout, check this: https://stackoverflow.com/questions/37318228/how-to-make-constraintlayout-work-with-percentage-values – Ali Nem Dec 08 '17 at 05:07

11 Answers11

124

I don't know if anyone is still reading this thread or not, but Jeff's solution will only get you halfway there (kinda literally). What his onMeasure will do is display half the image in half the parent. The problem is that calling super.onMeasure prior to the setMeasuredDimension will measure all the children in the view based on the original size, then just cut the view in half when the setMeasuredDimension resizes it.

Instead, you need to call setMeasuredDimension (as required for an onMeasure override) and provide a new LayoutParams for your view, then call super.onMeasure. Remember, your LayoutParams are derived from your view's parent type, not your view's type.

@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);
}

I believe the only time you'll have problems with the parent LayoutParams is if the parent is an AbsoluteLayout (which is deprecated but still sometimes useful).

Sibbs Gambling
  • 19,274
  • 42
  • 103
  • 174
Vic
  • 5,977
  • 2
  • 22
  • 19
  • 12
    In my experience, this rarely works, esp. when fill/match_parent or weights are invoked. Better invoke measureChildren after setMeasuredDimension (with e.g. MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST)). – M. Schenk Dec 21 '11 at 14:55
  • I need to change a TextView´s left margin instead of its size depending on parent layout´s width. Using your code it works more or less, but it gives me strange results when using it in the rows of a ListView, i. e. it´s not always in the correct position after scrolling. Do I have to override another method? How should I call setMeasuredDimension in this case? – Konsumierer May 18 '12 at 15:47
  • 1
    Does `MeasureSpec.getSize(widthMeasureSpec)` really give the correct parent´s width? For me it just returns random widths which are quite close to the actual width, but not exact. – Konsumierer May 21 '12 at 10:18
  • 1
    @M.Schenk has it. I have a VideoView inside a weighted layout. I need the width to be a percentage of screen size, and I need to maintain the aspect ratio. This stretches the video, so `wrap_content` won't do. Vic's answer does not work for this scenario, but @M.Schenk's comment does. It also saves an extra layout pass. You should also post it as an answer for visibility. – dokkaebi Jan 10 '13 at 16:32
  • Like @Konsumierer said, for me `MeasureSpec.getSize()` returns random numbers. Only for me a lot of the time they are not close to the parent's height. Why is that? – Ben Kane Aug 02 '13 at 20:31
  • 7
    Won't the call to `super.onMeasure()` simply override and nullify the effects of the earlier call to `this.setMeasuredDimension()`? – Spinner Dec 17 '13 at 15:39
  • 1
    I'm also curious, as @Spinner mentioned, why the call to `super.onMeasure()` doesn't override the earlier calculations. – jwir3 Jun 24 '14 at 21:06
  • I'm calling super at the start, because it really overrides later calculations, and also you should not create new LayoutParams to set, but getLayoutParams, alter them, and then set. – B-GangsteR Aug 30 '18 at 12:52
43

You could solve this by creating a custom View and override the onMeasure() method. If you always use "fill_parent" for the layout_width in your xml then the widthMeasureSpec parameter that is passed into the onMeasusre() method should contain the width of the parent.

public class MyCustomView extends TextView {

    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
        this.setMeasuredDimension(parentWidth / 2, parentHeight);
    }
}   

Your XML would look something like this:

<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <view
        class="com.company.MyCustomView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
Khalid Ali
  • 373
  • 1
  • 15
Jeff
  • 674
  • 5
  • 6
29

I've found that it's best not to mess around with setting the measured dimensions yourself. There's actually a bit of negotiation that goes on between the parent and child views and you don't want to re-write all that code.

What you can do, though, is modify the measureSpecs, then call super with them. Your view will never know that it's getting a modified message from its parent and will take care of everything for you:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
    int myWidth = (int) (parentHeight * 0.5);
    super.onMeasure(MeasureSpec.makeMeasureSpec(myWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
}
itzmebibin
  • 9,199
  • 8
  • 48
  • 62
Phil Kulak
  • 6,960
  • 8
  • 45
  • 50
17

When you define a layout and view on XML, you can specify the layout width and height of a view to either be wrap_content, or fill_parent. Taking up half of the area is a bit harder, but if you had something you wanted on the other half you could do something like the following.

<LinearLayout android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ImageView android:layout_height="fill_parent"
        android:layout_width="0dp"
        android:layout_weight="1" />
    <ImageView android:layout_height="fill_parent"
        android:layout_width="0dp"
        android:layout_weight="1" />
</LinearLayout>

Giving two things the same weight means that they will stretch to take up the same proportion of the screen. For more info on layouts, see the dev docs.

Cheryl Simon
  • 46,552
  • 15
  • 93
  • 82
  • I don't recommend this, but...as a hack, if you do use the above approach you could make one of those views a "dummy" and set android:visibility="invisible" to fill half the area – RickNotFred Jan 29 '10 at 13:32
  • What is the purpose of only using half the area? Are you planning on displaying anything in the other half? What you do with the remaining area will inform the best approach – RickNotFred Jan 29 '10 at 13:33
  • this should definitely be #1 - if you can do it in your xml, why do it in your java? – fandang Dec 10 '14 at 16:17
9

I believe that Mayras XML-approach can come in neat. However it is possible to make it more accurate, with one view only by setting the weightSum. I would not call this a hack anymore but in my opinion the most straightforward approach:

<LinearLayout android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:weightSum="1">
    <ImageView android:layout_height="fill_parent"
               android:layout_width="0dp"
               android:layout_weight="0.5"/>
</LinearLayout>

Like this you can use any weight, 0.6 for instance (and centering) is the weight I like to use for buttons.

Community
  • 1
  • 1
pinkwerther
  • 313
  • 4
  • 10
3

Try this

int parentWidth = ((parentViewType)childView.getParent()).getWidth();
int parentHeight = ((parentViewType)childView.getParent()).getHeight();

then you can use LinearLayout.LayoutParams for setting the chileView's parameters

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(childWidth,childLength);
childView.setLayoutParams(params);
Abhishek Gupta
  • 157
  • 1
  • 3
2

You can now use PercentRelativeLayout. Boom! Problem solved.

androidguy
  • 3,005
  • 2
  • 27
  • 38
  • 2
    PercentRelativeLayout was deprecated in API level 26.0.0-beta1. – dzikovskyy Jul 20 '17 at 13:52
  • What do you mean "In API level"? Do you mean library version number? PRL is in the support library, has no API level. – androidguy Jul 20 '17 at 22:24
  • By API level I mean version of Android. Info from here https://developer.android.com/reference/android/support/percent/PercentRelativeLayout.html. More info about API level here https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels – dzikovskyy Jul 21 '17 at 06:19
  • You can still use this class unless for some reason you need version 26.0.0 or higher. – androidguy Jul 26 '17 at 01:55
1

Roman, if you want to do your layout in Java code (ViewGroup descendant), it is possible. The trick is that you have to implement both onMeasure and onLayout methods. The onMeasure gets called first and you need to "measure" the subview (effectively sizing it to the desired value) there. You need to size it again in the onLayout call. If you fail to do this sequence or fail to call setMeasuredDimension() at the end of your onMeasure code, you won't get results. Why is this designed in such complicated and fragile way is beyond me.

Pavel Lahoda
  • 219
  • 1
  • 4
  • 7
1

It's something like this:

<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"
    tools:context="com.company.myapp.ActivityOrFragment"
    android:id="@+id/activity_or_fragment">

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/linearLayoutFirst"
    android:weightSum="100">   <!-- Overall weights sum of children elements -->

    <Spinner
        android:layout_width="0dp"   <!-- It's 0dp because is determined by android:layout_weight -->
        android:layout_weight="50"
        android:layout_height="wrap_content"
        android:id="@+id/spinner" />

</LinearLayout>


<LinearLayout
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:layout_below="@+id/linearLayoutFirst"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true"
    android:id="@+id/linearLayoutSecond">

    <EditText
        android:layout_height="wrap_content"
        android:layout_width="0dp"
        android:layout_weight="75"
        android:inputType="numberDecimal"
        android:id="@+id/input" />

    <TextView
        android:layout_height="wrap_content"
        android:layout_width="0dp"
        android:layout_weight="25"
        android:id="@+id/result"/>

</LinearLayout>
</RelativeLayout>
Mario
  • 486
  • 4
  • 7
1

If the other side is empty. I guess the simplest way would be to add an empty view (e.g. a linear layout) then set both views' widths to fill_parent and both their weights to 1.

This works in a LinearLayout...

jpm
  • 3,300
  • 1
  • 19
  • 29
-1

I've been struggling this question for a long time, I want to change my DrawerLayout's drawer width, which is the second child of its parent. Recently I figured it out, but doesn't know if there's a better idea.

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    super.onLayout(changed, l, t, r, b)
    (getChildAt(1).layoutParams as LayoutParams).width = measuredWidth/2
}
John 6
  • 99
  • 1
  • 5
  • Problem with using this way is that setting layout params inside onLayout can lead to infinite recursion and UI thread may never become idle – Tushar Kathuria Jun 08 '21 at 06:26
  • @TusharKathuria How come? After this layout, child LP's width mode is EXACTLY, and it won't change any more. – John 6 Aug 21 '21 at 07:28