Thursday, August 16, 2012

Android: Relative Layout with weighted size

Android applications should support multiple screens, So absolute values cannot be used in layouts. Mostly Relative Layout or Liner Layout is used in UI arrangements, In Relative Layout, views can be arranged relatively. for example one button can be arranged  below  to another etc. But, only in Liner Layout, views' size can be specified with weight. i.e width of a view can be 70% of it parent width etc. This feature is very useful in supporting multiple screens. Since it applies weight in vertically or horizontally at a time normally Liner Layout need to be nested. This may reduce the performance.

So, It is better introducing weight attributes for Relative Layout and will improve application performance. If this can be done in Android API level it is easy since already an implementation is there for Relative Layout. I see this is an enhancement for Relative Layout and no need to introduce another Layout, otherwise same code will be duplicated.

Lets' try this with an example, Say If you want to create a layout as below:

The expectation is as below: setting layout_width=0dip and introducing layout_width_weight and set required value.


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/button01"
        android:layout_width="0dip"
android:layout_width_weight="0.2500"
        android:layout_height="0dip"
android:layout_height_weight="0.1666"
        android:text="Button01" />

    <Button
        android:id="@+id/button02"
        android:layout_width="0dip"
android:layout_width_weight="0.2500"
        android:layout_height="0dip"
android:layout_height_weight="0.1666"
        android:layout_below="@+id/button01"
        android:text="Button02" />

    <Button
        android:id="@+id/button03"
        android:layout_width="0dip"
android:layout_width_weight="0.2500"
        android:layout_height="0dip"
android:layout_height_weight="0.1666"
        android:layout_below="@+id/button02"
        android:text="Button03" />

    <Button
        android:id="@+id/button04"
        android:layout_width="0dip"
android:layout_width_weight="0.5000"
        android:layout_height="0dip"
android:layout_height_weight="0.5000"
        android:layout_centerInParent="true"
        android:text="Button04" />

    <Button
        android:id="@+id/button05"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_alignParentRight="true"
        android:layout_below="@id/button04"
        android:text="Button05" />

</RelativeLayout>

I checked for necessary changes in Relative Layout source code and found that,

1. Both fields for holding weight values need to be added in RelativeLayout.LayoutParams class and loading this values into it.
2. new Parameter need  to be added to RelativeLayout.getChildMeasureSpec method inorder to pass the value from RelativeLayout.measureChild and RelativeLayout.measureChildHorizontal methods.
3. Add extra condition if child size equals to zero then make use of the weight value inside RelativeLayout.getChildMeasureSpec to calculate the size.

private int getChildMeasureSpec(..., float weight){
...
 if (childSize == 0) {
     childSpecMode = MeasureSpec.EXACTLY;
     childSpecSize = (int)(mySize*weight);
}else if(chileSize>0){
....
}
...
}

But before going for a Android API level change, It is better to try out. It is impossible since it a private method, it cannot be overridden.

But this methods are called only in onMeasure, So I created a new layout by extending Relative Layout and copy source from Android API for those 4 methods(onMeasure, measureChild, measureChildHorizontal and getChildMeasureSpec), and since these methods use some private/hide methods/field from RelativeLayout and other Android API, I used Java Reflection to access them.

Now, Since weight attributes also not added into API using  those in XML also impossible. But lucky I found that negative values also can be set to layout_width and layout_height and only -1,-2 has used. So I try to use the negative values to indicate those weight values and modify the condition in getChildMeasureSpec according to that.

I found more that, if we set negative px value then childSize will be one less than the value set in XML. Since childSize is integer number and get more precision I go for 10,000 scaled weighted values.

So the layout xml will be like this.


<?xml version="1.0" encoding="utf-8"?>
<demo.widget.WeightedSizeRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/button01"
        android:layout_width="-2501px"
        android:layout_height="-1667px"
        android:text="Button01" />

    <Button
        android:id="@+id/button02"
        android:layout_width="-2501px"
        android:layout_height="-1667px"
        android:layout_below="@+id/button01"
        android:text="Button02" />

    <Button
        android:id="@+id/button03"
        android:layout_width="-2501px"
        android:layout_height="-1667px"
        android:layout_below="@+id/button02"
        android:text="Button03" />

    <Button
        android:id="@+id/button04"
        android:layout_width="-5001px"
        android:layout_height="-5001px"
        android:layout_centerInParent="true"
        android:text="Button04" />

    <Button
        android:id="@+id/button05"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_alignParentRight="true"
        android:layout_below="@id/button04"
        android:text="Button05" />

</demo.widget.WeightedSizeRelativeLayout>


and the necessary changes for getChildMeasureSpec method is this:

private int getChildMeasureSpec(...){
........

         if (childSize < -2 ) {
                          // Child wanted an exact size. Give as much as possible
                          childSpecMode = MeasureSpec.EXACTLY;

                          childSpecSize = (int) (mySize*(-1.0f*childSize/10000));
                 
        }else if (childSize >= 0) {
.........

}


I have uploaded both Java and demo xml here.

Let test this and hope necessary implementation for adding weight attribute into Relative Layout Parameter will be added.

8 comments:

Anonymous said...

Hello Sudar!

Thank you! This is great stuff!

However, I run into an error. When the App is trying to set up the layout, I get a NullPointerException in this line:

final int layoutDirection = (Integer) invokePrivateMethod("getResolvedLayoutDirection", null);

If I change the code and set layoutDirection to any int value, e.g. 0 or 1, the layout is rendered but all the android:layout_below and such are ignored so that everything is laying over each other.

Do you have any idea how to solve this?

Sudar Nimalan said...

Could you send or post the whole stack trace

Anonymous said...

Hello Sudar!

Thanks for your reply!

This is my stacktrace:

ERROR/AndroidRuntime(22330): FATAL EXCEPTION: main
java.lang.NullPointerException
at com.infinit.Device2MAP.Custom.WeightedRelativeLayout.onMeasure(WeightedRelativeLayout.java:223)
at android.view.View.measure(View.java:15518)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4825)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:15518)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4825)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:15518)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4825)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1404)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:695)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:588)
at android.view.View.measure(View.java:15518)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4825)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at com.android.internal.policy.impl.PhoneWindow$DecorView.onMeasure(PhoneWindow.java:2176)
at android.view.View.measure(View.java:15518)
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:1874)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1089)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1265)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:989)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4351)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749)
at android.view.Choreographer.doCallbacks(Choreographer.java:562)
at android.view.Choreographer.doFrame(Choreographer.java:532)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5041)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
at dalvik.system.NativeStart.main(Native Method)

Sudar Nimalan said...

Looks you have a empty layout, there are no child added to the layout?

knucKles said...
This comment has been removed by the author.
Anonymous said...

In my onCreateView() of the Fragment, the call inflater.inflate(R.layout.fragment_project, container, false) can not find the layout. If I remove your weighted relative layout from the XML file, it works - but without this great relative layout. :(

Maybe we can discuss this further via eMail. Where can I find your address to contact you?

Unknown said...

Your blog is very useful for me, as your tutorial sessions are indeed of great benefit.
Android Training placement | Android Training in chennai |Android Training in Velachery | android development course fees in chennai

Unknown said...

Android applications should support multiple screens, So absolute values cannot be used in layouts. Mostly Relative Layout or Liner Layout is used in UI arrangements, In Relative Layout, views can be arranged relatively..
Android Training in chennai | Android Training|Android Training in chennai with placement | Android Training in velachery