Showing posts with label Liner Layout. Show all posts
Showing posts with label Liner Layout. Show all posts

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.

Monday, June 18, 2012

Android: LinearLayout, Dynamically Arranging Views

Let see how to arrange buttons according to the user selection as shown below:


<LinearLayout android:orientation="vertical" android:id="@+id/linearLayout1"
     android:layout_height="fill_parent" android:layout_width="fill_parent">
    <Button android:text="Button" android:id="@+id/button0"
          android:layout_height="fill_parent" android:layout_width="fill_parent"
          android:layout_weight="1">Button>
     <Button android:text="Button" android:id="@+id/button1"
          android:layout_height="fill_parent" android:layout_width="fill_parent"
          android:layout_weight="1" android:visibility="gone">Button>
     <Button android:text="Button" android:id="@+id/button2"
          android:layout_height="fill_parent" android:layout_weight="1"
          android:layout_width="fill_parent" android:visibility="gone">Button>
LinearLayout>


Following java code would change the visibility according to the user selection.

@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
     switch(group.getCheckedRadioButtonId()){
     case R.id.radio0:
          button0.setVisibility(View.VISIBLE);
          button1.setVisibility(View.GONE);
          button2.setVisibility(View.GONE);
          break;
     case R.id.radio1:
          button0.setVisibility(View.VISIBLE);
          button1.setVisibility(View.VISIBLE);
          button2.setVisibility(View.GONE);
          break;
     case R.id.radio2:
          button0.setVisibility(View.VISIBLE);
          button1.setVisibility(View.VISIBLE);
          button2.setVisibility(View.VISIBLE);
          break
      }
}

Let see if we want a layout to change as show below:
This can be down in two ways.
Option 01: changing the layout XML file to use the weight sum:

<LinearLayout android:orientation="vertical" android:id="@+id/linearLayout1"
     android:layout_height="fill_parent" android:layout_width="fill_parent" android:weightSum="3">
     <Button android:text="Button" android:id="@+id/button0"
          android:layout_width="fill_parent"
          android:layout_weight="1" android:layout_height="0dip">Button>
     <Button android:text="Button" android:id="@+id/button1"
          android:layout_width="fill_parent"
          android:layout_weight="1" android:visibility="gone" android:layout_height="0dip">        Button>
     <Button android:text="Button" android:id="@+id/button2"
          android:layout_weight="1"
          android:layout_width="fill_parent" android:visibility="gone" android:layout_height="0dip">Button>
LinearLayout>

Option 02: using java code:

@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
     switch(group.getCheckedRadioButtonId()){
     case R.id.radio0:
          button0.setVisibility(View.VISIBLE);
          button1.setVisibility(View.INVISIBLE);
          button2.setVisibility(View. INVISIBLE);
          break;
     case R.id.radio1:
          button0.setVisibility(View.VISIBLE);
          button1.setVisibility(View.VISIBLE);
          button2.setVisibility(View. INVISIBLE);
          break;
     case R.id.radio2:
          button0.setVisibility(View.VISIBLE);
          button1.setVisibility(View.VISIBLE);
          button2.setVisibility(View.VISIBLE);
          break
      }
}

option 01 you should change the xml file and in option 02 you should change the java code.

Saturday, May 19, 2012

Android: Expand Liner Layout Beyond its Width

In android, the layout weight parameter in the liner layout is very useful to layout views with relative ratios. Say you want to arrange two frames left and right, and right frame take quoter and right take the balance.


We can simply create this using following layout xml. Only thing is to noted down is, the width should be set to 0dip and weight to be set to 0.25 or 0.75 according to the requirement. That all.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
    android:weightSum="1" >

    <FrameLayout
        android:layout_width="0dip"
        android:layout_height="match_parent"
        android:layout_weight="0.75"
        android:background="#FF0000" >
    </FrameLayout>

    <FrameLayout
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.25"
        android:background="#00FF00" >

        <ToggleButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    </FrameLayout>

</LinearLayout>


But in our case, we wanted to add another frame next to the right frame its width also 0.75 of the screen and in some user action, by moving the screen, the left frame should be hidden and 3rd frame should be shown.



So we created custom liner layout by extending the liner layout, where we override the on measure method to adjust its size by 0.75 of its size.


public class ExpandedLinearLayout extends LinearLayout {
@SuppressWarnings("unused")
private static final String TAG = "demo.beyond.layout.ExpandedLinearLayout";
public ExpandedLinearLayout(Context context) {
super(context);
}
public ExpandedLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExpandedLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension((int) (1.75f*getMeasuredWidth()), getMeasuredHeight());
}
}



And add another new frame to above layout with weight 0.75.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
.................    
    <FrameLayout
        android:layout_width="0dip"
        android:layout_height="match_parent"
        android:layout_weight="0.75"
        android:background="#0000FF" >
    </FrameLayout>

</LinearLayout>


Then we add the animation for the on click.


public class DemoExpandLayoutActivity extends Activity implements OnClickListener {
        private LinearLayout mLinearLayout;
private FrameLayout mFrameLayout1;
private ToggleButton mToggleButton;
/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLinearLayout = (LinearLayout)findViewById(R.id.frame);
        mFrameLayout1 = (FrameLayout)findViewById(R.id.frameLayout1);
        mToggleButton = (ToggleButton)findViewById(R.id.toggleButton1);
        mToggleButton.setOnClickListener(this);
    }

@Override
public void onClick(View arg0) {
float factor = 1.0f*mFrameLayout1.getWidth()/mLinearLayout.getWidth();
if(mToggleButton.isChecked()){
mLinearLayout.setTranslationX(-mFrameLayout1.getWidth());
TranslateAnimation translateAnimation = new TranslateAnimation(
TranslateAnimation.RELATIVE_TO_PARENT,
factor,
TranslateAnimation.RELATIVE_TO_PARENT,
0.0f,
TranslateAnimation.RELATIVE_TO_PARENT,
0.0f,
TranslateAnimation.RELATIVE_TO_PARENT,
0.0f);
translateAnimation.setDuration(1000);
LayoutAnimationController layoutAnimationController = new LayoutAnimationController(translateAnimation,0.1f);
mLinearLayout.setLayoutAnimation(layoutAnimationController);
}else{
mLinearLayout.setTranslationX(0);
TranslateAnimation translateAnimation = new TranslateAnimation(
TranslateAnimation.RELATIVE_TO_PARENT,
-factor,
TranslateAnimation.RELATIVE_TO_PARENT,
0.0f,
TranslateAnimation.RELATIVE_TO_PARENT,
0.0f,
TranslateAnimation.RELATIVE_TO_PARENT,
0.0f);
translateAnimation.setDuration(1000);
LayoutAnimationController layoutAnimationController = new LayoutAnimationController(translateAnimation,-0.1f);
mLinearLayout.setLayoutAnimation(layoutAnimationController);
}
}
}


But that didn't quit worked out, because liner layout ignores views If those out side the weight sum.

But if we add views as match patent, with layout weights, liner layout behaves differently. It draws all the views beyond its boundary.
In this case the actual width of each view was not proportional to layout weight what we set in layout XML.
So to find out the relation, I just try for different values and plot the following graph and got the below equation.



I couldn't find any direct evidence in android code base or may have to check some more android source code. But this is good enough to calculate the proper values for us.

Then I changed the layout XML with those values.


<?xml version="1.0" encoding="utf-8"?>
<demo.beyond.layout.ExpandedLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/frame"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
    android:weightSum="1" >

    <FrameLayout
        android:id="@+id/frameLayout1"
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.125"
        android:background="#FF0000" >
    </FrameLayout>

    <FrameLayout
        android:id="@+id/frameLayout2"
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.375"
        android:background="#00FF00" >

        <ToggleButton
            android:id="@+id/toggleButton1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    </FrameLayout>

    <FrameLayout
        android:id="@+id/frameLayout3"
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.125"
        android:background="#0000FF" >
    </FrameLayout>

</demo.beyond.layout.ExpandedLinearLayout>


Further more I modified the expanded liner layout with this formula, so that it can use in all cases.


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int numberOfChild = getChildCount();
float totalWidth = 0;
for(int i = 0;i < numberOfChild;i++){
totalWidth+=getMeasuredWidth()
*(1 - 2*((LinearLayout.LayoutParams)getChildAt(i).getLayoutParams()).weight);
}
setMeasuredDimension((int) totalWidth, getMeasuredHeight());
}


Here I have uploaded the source code, try it your self and let me know your feedback or let me know if you have any other option.