Wednesday, August 22, 2012

Android: Layout Transition CHANGING animation for API level 11-15

In android, Layout Transition is introduced from API level 11. By enabling layout transition application can get predefine platform animations for following changes.


1. APPEARING - animation that runs on those items that are appearing in the container.
2. CHANGE_APPEARING - animation that runs on those items that are changing due to a new item appearing in the container.
3. CHANGE_DISAPPEARING - animation that runs on those items that are changing due to an item disappearing from the container.
4. DISAPPEARING - animation that runs on those items that are disappearing from the container.


These are fast, tested, easy to incorporate with our applications.

CHANGING animation was not added to API at that time, but it has been added now in API level 16.

CHANGING - animation that runs on those items that are changing due to a layout change not caused by items being added to or removed from the container.

So incase if we want to support devices in API level 11, it is good to have some work around, and it should be able to get platform animation in other decides more than API level 16.

I am trying to make a utility for this to simply incorporate this and here I have two options.

1. Simple One time registration/setup, but this will not support scroll changes or any other property changes. only support left, top, right and bottom.

First we register on layout change listener for both layout and its children. So as soon as there is a layout change in these views the callback will be called.


private static void setupChangeAnimationOneTime(final View view) {
...
final View.OnLayoutChangeListener onLayoutChangeListener = new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
...

}
};
view.addOnLayoutChangeListener(onLayoutChangeListener);
...
}


Here, create a object animator, with old and new values.


PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left", oldLeft, left);
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", oldTop, top);
PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt("right", oldRight, right);
PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", oldBottom, bottom);
ObjectAnimator changeAnimator = ObjectAnimator.ofPropertyValuesHolder((Object)view,
                pvhLeft, pvhTop, pvhRight, pvhBottom);
changeAnimator.setDuration(600);


Start the animator.


changeAnimator.start();


In this option, we don't know other property of the view. so we cannot animate them.

2. Advanced approach, setup in each layout changes, for this the root layout needed to be a custom layout, where the onlayout method should be overridden or need to find a proper action where the layout change is happened (example onClick etc).


First create an animator with properties you wanted animate. Use null for target object and some arbitrary values, which will be changed in setup.



public static ObjectAnimator getDefaultChangeAnimator(){
if(sDefaultChangeAnimator==null){
        PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left", 0, 1);
        PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", 0, 1);
        PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt("right", 0, 1);
        PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1);
        PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1);
        PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1);
        sDefaultChangeAnimator = ObjectAnimator.ofPropertyValuesHolder((Object)null,
                pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY);
        sDefaultChangeAnimator.setDuration(600);
}
return sDefaultChangeAnimator;
}


Now the animator has to be setup just before the layout change of the layout and it's children. First start state will be setup and layout change listener wil be registered.


private static void setupChangeAnimation(final View view) {

changeAnimator.setTarget(view);

changeAnimator.setupStartValues();
final View.OnLayoutChangeListener onLayoutChangeListener = new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
...
changeAnimator.setupEndValues();
changeAnimator.start();

};
changeAnimator.addListener(animatorListener);
view.addOnLayoutChangeListener(onLayoutChangeListener);
...
}

after layout change, inside the on layout change callback the end state of the animator will be setup and start immediately.

Here  we need to make sure setupChangeAnimation called just before the layout change. For this we have two possible place,
1. inside the onlayout call of animation enabled layout.
2. posible action code that make layout changes. (example onClick())


public class DemoLinearLayout extends LinearLayout {
...
protected void onLayout(boolean changed, int l, int t, int r, int b) {
LayoutTransitionUtil.checkAndSetupChangeAnimationForAllChild(this);
super.onLayout(changed, l, t, r, b);
}
}


@Override
public void onClick(View view) {
...
LayoutTransitionUtil.setupChangeAnimation((ViewGroup)findViewById(R.id.frm_linear_04));
}



Here I am uploading the sample source code for reference.

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.