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.