GradientDrawable on Android

Drawable represents something that can be drawn on screen on Android. There’re many Drawable types. This article covers GradientDrawable, which can also be defined in xml with <shape> element.

Confusingly there’s also ShapeDrawable, which the Android documentation and training guide claims to be defined in xml with <shape> element. This is not right and one can look at ShapeDrawable source code to confirm that.

The following screenshot captures what can be done with GradientDrawable. The source can be found here.

Figure 1. Drawings Generated by Android GradientDrawable

Shape

And the table below shows some elements are not available for certain shapes.

Element\Shape Line Rectangle Oval Ring
corners NA NA NA
Linear gradient NA
Radial gradient NA
Sweep gradient NA
padding
size
solid NA
stroke

We’ll first introduce the four shapes, and then go through some of the important xml elements.

Line

There’s not much use when shape is line, because lots of elements are not supported and all the effects we want to draw with line shape can be done with rectangle.

One thing to note that is when specifying a dashed stroke, it won’t work properly due to bug here. Suppose we’re drawing the line on ImageView, the following code is needed.

 imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

Rectangle

Rectangle is used to draw a rectangle-like shape. It’s the only shape can have corners elements, which makes it very powerful. The corners element allows one to specify the radius at four corners of the rectangle.

One can use rectangle shape to draw a line by specifying a small height, or draw a circle by specifying proper corner radius.

Oval

Oval can be used to draw a circle or oval.

Ring

Ring is used to draw a ring with specified thickness. innerRadius, innerRadiusRatio, thickness, thicknessRatio, useLevel attributes only applies when the shape attribute is ring.

innerRadius is used to specify the width of the inner hole of the ring. innerRadiusRatio is to specify the innerRadius as the ratio of the ring’s width. For example, innerRadiusRatio=”5” means innerRadius is equal to the ring’s width divide by 5.

Thickness defines the thickness of the ring. thicknessRatio is similar to innerRadiusRatio.

Gradient

There are three gradient types supported, namely linear, radial and sweep. One can refer to figure 1 for the drawing can be created by them.

All gradients must specify startColor and endColor. centerColor is optional and its relative position is also configurable via centerX and centerY (both in range of (0, 1.0)).

Linear

A linear gradient, which specifies a starting gradually transforms into ending color. It is also the default gradient. Linear gradient has a special attribute named angle. It must be a multiple of 45 and in the range of [0, 315].

Figure 2. Angle of Linear Gradient

Radial

Radial defines a gradient in a fan out fashion. It has a special attribute gradientRadius, which must be defined.

Padding

Padding is applied to the containing View element (this pads the position of the View content, not the shape).

For example, if this oval is set to the background of an ImageView, and the paddings will be applied to position the image resource (view content) of the image view.

Size

Defines the shape’s width and height. The shape scales to the size of the container View proportionate to the dimensions defined here by default. When you use the shape in an ImageView, you can restrict scaling by setting the android:scaleType to “center”.

References

  1. GradientDrawable Android doc
  2. Android drawable resource shape

Choose the Right Clock on Android

Android framework provides three different time keeping facilities. This post discusses the main differences and usage of them.

System.currentTimeMillis

It returns the number of milliseconds elapsed since the epoch, which is 00:00:00 January 1, 1970 UTC.

Note that this clock is not guaranteed to be monotonic, because of the followings:

  • The clock can be changed by network time sync
  • The clock can be updated by user or program

The clock can jump backwards or forwards unpredictably. We can listen for ACTION_TIME_TICK,ACTION_TIME_CHANGED and ACTION_TIMEZONE_CHANGED Intent broadcasts to find out when the time changes.

When to use

  • Get the wall clock time
  • Inerval/elapsed time that needed to span across device reboot. This is not ideal, but a cheap and OK solution sometimes. Ideally we want to have a server provided timestamp to avoid all the unpredictably jump can happen to this clock.

When not to use

  • Interval or elapsed timing doesn’t need to span across device reboot. E.g.: memory cache expiration time

SystemClock.uptimeMillis

It returns the number of milliseconds since device boot. The clock stops when system enters deep sleep. The clock is guaranteed to be monotonic.

This clock is the basis for Thread.sleep(millls), Object.wait(millis), and System.nanoTime().

When to use

  • Interval or elapsed timing doesn’t need to span across device reboot, and we want to exclude device deep sleep

SystemClock.elapsedRealtime

It returns the number of milliseconds since device boot. The clock don’t stop even when system enters deep sleep. It’s also guaranteed to be monotonic.

elapsedRealtimeNanos(): Similar to method above, but returns number of nano seconds.

When to use

  • Interval or elapsed timing doesn’t need to span across device reboot, and we want to include device deep sleep

Caveats

1. When using System.nanoTime or elapsedRealtimeNanos, differences span greater than approximately 292 years (263 nanoseconds) will not be computed correctly due to numerical overflow.

2. When comparing two timestamps, we should use t2 – t1 > 0 instead of t2 > t1, because of possible numerical overflow.

In order to help us understand why t2 – t1 is better than t2 > t1 in case of numerical overflow, let’s assume that t1 and t2 are bytes within the range of [-128, 127].
When t1 = 126, t2 = t1 + 3 will result in numerical overflow, and t2 = -127. If we use t2 > t1, it will return false. But t2 – t1 = -127 – 126, which will again result in numerical overflow, and the result is 3.

This only works when the difference of t2 and t1 itself can be represented by the data type. In the byte example above, t2 – t1 must be less than or equal to 127.

ArrayMap and its friends in Android

Previous post we discussed about SparseArray and its friends in Android. While they can replace HashMap when integer and long are used as key. It doesn’t work when the key is an arbitrary object.

Actually similar ideas can be applied to create more memory efficient generic map data structure. This post will go through ArrayMap, ArraySet and their friends, which replaces HashMap and HashSet in a more memory efficient way.

Usage

ArrayMap implements Map<K, V> interface, and it’s fully compatible with HashMap. There’a simplified version of ArrayMap: SimpleArrayMap, which doesn’t have the burden of maintaining compatibility with Java Map inteface, but still provides most of the commonly needed methods.

Correspondingly there’s ArraySet implements the Set<V> interface.

We should consider using ArrayMap and ArraySet at the following cases.

1. The number items is less than 1000.
2. Frequent reading, insertion and removal are less frequent.

And if we don’t need to maintain compatibility with Java interface, we can use SimpleArrayMap where ArrayMap is used.

Internals

We have described how HashMap works internally at SparseArray and its friends in Android. Basically it uses an array of linked list, and lots of unused space is allocated in the array.

For ArrayMap, two arrays are used. One is used to store the hash code of all keys, and the other is used to store the key value pairs. Key is stored at the even index, and value is saved at odd index.

Figure 1. Internal arrays in ArrayMap [image from ref 1]

Binary search is applied on the hash code array to locate the index, and the same index is used in the second array to store and retrieve key and value. Specifically,

1. For get, the hash code of key is computed and used in binary search to find the index. Then two searches are performed to find the equal key in the second array. One is searching forward, and the other is search backward. If no equal key is found, null is returned.

2. For put, if an equal key is found, the value is updated; otherwise a new key, value pair is inserted at the end of the equal hash code range. Array resizing might be applied in the process.

3. ArrayMap internally caches the arrays of size 4 and 8, to avoid spamming garbage. This further improves the memory efficiency when number of items in the map is small.

Advantages and Disadvantages

The internals of ArrayMap offers the following advantages compared with HashMap.

1. It’s more memory efficient. There’s no unused space allocated.

2. When there’s no element, ArrayMap doesn’t allocate memory, whereas HashMap allocates unused space.

3. Iteration with keyAt and valueAt is just like iterating an array. It’s very efficient. HashMap uses Iterator, which is slower and takes more memory.

But these advantages are not free,

1. Accessing a value from ArrayMap is O(logn), whereas in HashMap is O(1). This makes not much difference when number of elements in the Map is less than 1000.

2. Insertion and Deletion are relatively expensive, since we need to shift array elements, or allocate new array and copy elements over.

References

1. Android Fun with ArrayMaps performance video.

SparseArray and its friends in Android

One most important factor affecting Android app performance is memory. Android framework provides some speical classes to help developers optimizating memory usage. SparseArray and its friends are some of them.

How to use

SparseArray and its friends are designed to replace HashMap in Android when there isn’t large number of elements in the map. Below we list outs the Java usage and its corresponding Android replacement.

SparseArray          <Integer, Object>
SparseBooleanArray   <Integer, Boolean>
SparseIntArray       <Integer, Integer>
SparseLongArray      <Integer, Long>
LongSparseArray      <Long, Object>
LongSparseLongArray  <Long, Long>

Note that Android support library have two classes named SparseArrayCompat and LongSparseArray for backward compatibility. SparseArrayCompat supports methods like removeAtRange, which is only available at SparseArray from API 19.

When to use

In a word, we should use SparseArray and its friends when both of the following are met,
1. The key of the map is integer or long.
2. The number of elements is less than several hundreds.

This is because of the following advantages offered by SparseArray.
1. The key is primitive type, so putting an element doesn’t require creating an aditional Entry object, while HashMap does.
2. Also because key is primitive, there’s no auto-boxing of keys.

According to Android Memories presentation, for holding 1000 elements, SparseArray uses 8k while HashMap uses 64k.

class HashMap<k, v="">{ //  Class = 12 + 8 * 4 = 48 bytes
    Entry<k, v="">[] table; // Entry = 32 + 16 + 16 = 64 bytes
    Entry<k, v="">[] forNull; //  Array = 20 + 1000 * 64 = 64024 bytes
    int size;
    int modCount;
    int threshold;
    Set keys;
    Set<entry<K,V>> entries; // Total = 64,136 bytes
    Collection values;
}

class SparseIntArray { //  Class = 12 + 3 * 4 = 24 bytes
    int[] keys;  //  Array = 20 + 1000 * 4 = 4024 bytes
    int[] values;
    int size; //  Total = 8.072 bytes
}

But the memory efficiency doesn’t come for free.
1. HashMap computes the hash code to find the index to insert a new element in O(1). SparseArray uses binary search to find the index to insert in O(logn), which is less time efficient. According to Android official documentation, this difference is less than 50% for up to hundreds of items.
2. SparseArray is Android special, meaning less portable.

In addition, SparseArray offers some additional features.
1. The keys put into SparseArray are sorted ascendingly.
2. We can get an individual key or value given an index.

In contrast, HashMap doesn’t guarantee any order of the map. In fact, the order can change over time.

Internals

The pros and cons of SparseArray vs HashMap is due to their implementation.

For SparseArray, keys and values are kept in separate arrays.

1. Adding an element will use a binary search to find the insertion position.
a. If there’s already an element for key, replace it.
b. If there’s not and insertion space is available because the element there is marked as DELETED, insert it there.
c. Otherwise, we need to move array elements and grow the array capacity if necessary, and then insert the element.

2. Remove an element is optimized. The class simply mark it as DELETED. And when we query the size, the array needs to grow size, or do anything required to have a correct index, the array is then actually compacted.

Note that the method to compact the array is called gc, though it doesn’t necessarily will cause Java runtime GC to happen.

3. Because keys and values are kept in separate arrays and inserted using binary search position, the keys are guaranteed to be sorted.

HashMap is implemented using an array of linked list.

1. Adding an element will cause the class to compute the hash value of the key object (another hash is done on top of the hash code to further avoid collision). Based on the hash code, an array index is found to insert the value.

At each array index, there’s a linked list of Entry object, with key, value, hash and a pointer to next Entry object. When the same key (same reference or equals) is used, the old value is updated. Otherwise, a new Entry object is created, and added to the linked list.

2. Remove an element is similar. Finding the index based on hash in the array and traversal the linked list to find the correct Entry object to remove.

For a class with good hashcode implementation, the number of collisions will be small and the linked list at each array index will be short.

Android LayerDrawable and Drawable.Callback

LayerDrawable is a Drawable that manages an array of Drawables, where each Drawable is a layer of it. It can be used to compose fancy visual effects. However, used incorrectly, it can also introduce difficult to catch bugs. This post discusses a bug that can be caused by the Callback mechanism of Drawable.

Chain of Callbacks

In LayerDrawable, every layer/drawable registers the LayerDrawable as its Drawable.callback. This allows the layer to inform the LayerDrawable when it needs to redraw. As shown in the invalidateSelf method from Drawable.java, the DrawableCallback.invalidateDrawable is called to inform the client (the LayerDrawable, in this case) the drawable needs to redraw.

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

 

Also, a View registers itself as Drawable.callback, so when the drawable needs to redraw, the View can be informed and invalidated. So if we set background of a View to be LayeredDrawable, we have a chain of DrawableCallbacks. This is illustrated in the figure below.

1.jpg

Figure 1. Chain of Callbacks when Setting LayerDrawable as View’s Background

When the Drawable needs to redraw, it calls its callback.invalidateDrawable, which in turns calls LayerDrawable’s callback.invalidateDrawable, which causes the View to redraw the corresponding region if necessary.

View Breaks Callback of Old Background Drawable

So far all work well, but the setBackgroundDrawable of View.java has the following code snippet.

public void setBackgroundDrawable(Drawable background) {
    ...

    /*
     * Regardless of whether we're setting a new background or not, we want
     * to clear the previous drawable.
     */
    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    …
    if (background != null) {
        background.setCallback(this);
    }
    ...
}

This means setting a new background will break the old background drawable’s callback by setting it to null, regardless whether the callback is still set to the View.

The Bug

With all the knowledge above, we can “fabricate” a bug by follow the steps below.

  1. Set a Drawable A as a background of a View V. The A.callback is set to V.

  2. Create a LayerDrawable L with A as one layer. Now A.callback is set to L.

  3. Now set another Drawable (or just null) as background of V. V will set callback of its old background (A in our example) to null, which breaks the link between A and L.

  4. Updates A won’t trigger L to update.

The solution to this issue is to update the background of V before creating L. This is illustrated in the code example here.

In the example, we tried animating the actionbar background. The background consists of two layers, the first one is a green ColorDrawable, and second a bitmap drawable. We want to animate the alpha value of the bitmap drawable, so it appears the image is faded in.

The code contains two methods, one works and the other doesn’t due to the bug we described. Each method can be triggered by a button. Below we extract the key part of the code, one can refer to the link for more details.


Button btn1 = (Button) findViewById(R.id.button1);
btn1.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarWorking();
  }
});
Button btn2 = (Button) findViewById(R.id.button2);
btn2.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarNotWorking();
  }
});

private void animateActionBarNotWorking() {
	Drawable[] layers = new Drawable[] { colorLayer, launcherIconDrawable };
	// 2. This sets launcherIconDrawable.callback to layerDrawable
	LayerDrawable layerDrawable = new LayerDrawable(layers);
	// 3. This sets launcherIconDrawable.callback to null
	actionBar.setBackgroundDrawable(layerDrawable);
	ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 255);
	valueAnimator.setDuration(1000);
	valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
	  @Override
	  public void onAnimationUpdate(ValueAnimator animation) {
		// 4. Updates launcherIconDrawable will not trigger action bar background to update
		// as launcherIconDrawable.callback is null
		launcherIconDrawable.setAlpha((Integer) animation.getAnimatedValue());
	  }
	});
	valueAnimator.start();
}

private void animateActionBarWorking() {
	actionBar.setBackgroundDrawable(null);
	animateActionBarNotWorking();
}

 

References:

  1. LayerDrawable: http://developer.android.com/reference/android/graphics/drawable/LayerDrawable.html

How to Build ffmpeg with NDK r9

This is a updated post for a previous post, where we built ffmpeg 0.8 with Android NDK r5 and r6. This post will give instructions of how to build ffmpeg 2.0.1 with Android NDK r9.

0. Download Android NDK

The latest version of Android NDK can be downloaded at Android NDK website. At the time of writing, the newest version is NDK r9. Note that the website provides both current and legacy toolchains. We only need the current toolchain to compile ffmpeg.

After download NDK, simply decompress the archive. Note that we’ll use $NDK to represent the root path of the decompressed NDK.

1. Download ffmpeg source code

FFMPEG source code can be downloaded from the ffmpeg website. The latest stable release is 2.0.1. Download the source code and decompress it to $NDK/sources folder. We’ll discuss about the reason for doing this later.

2. Update configure file

Open ffmpeg-2.0.1/configure file with a text editor, and locate the following lines.

SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'

LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'

SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'

SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

This cause ffmpeg shared libraries to be compiled to libavcodec.so.<version> (e.g. libavcodec.so.55), which is not compatible with Android build system. Therefore we’ll need to replace the above lines with the following lines.

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'

LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'

SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'

SLIB_INSTALL_LINKS='$(SLIBNAME)'

3. Build ffmpeg

Copy the following text to a text editor and save it as build_android.sh.

#!/bin/bash

NDK=$HOME/Desktop/adt/android-ndk-r9

SYSROOT=$NDK/platforms/android-9/arch-arm/

TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64

function build_one

{

./configure 

    --prefix=$PREFIX 

    --enable-shared 

    --disable-static 

    --disable-doc 

    --disable-ffmpeg 

    --disable-ffplay 

    --disable-ffprobe 

    --disable-ffserver 

    --disable-avdevice 

    --disable-doc 

    --disable-symver 

    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- 

    --target-os=linux 

    --arch=arm 

    --enable-cross-compile 

    --sysroot=$SYSROOT 

    --extra-cflags="-Os -fpic $ADDI_CFLAGS" 

    --extra-ldflags="$ADDI_LDFLAGS" 

    $ADDITIONAL_CONFIGURE_FLAG

make clean

make

make install

}

CPU=arm

PREFIX=$(pwd)/android/$CPU 

ADDI_CFLAGS="-marm"

build_one

We disabled static library and enabled shared library. Note that the build script is not optimized for a particular CPU. One should refer to ffmpeg documentation for detailed information about available configure options.

Once the file is saved, make sure the script is executable by the command below,

sudo chmod +x build_android.sh

Then execute the script by the command,

./build_android.sh

4. Build Output

The build can take a while to finish depending on your computer speed. Once it’s done, you should be able to find a folder $NDK/sources/ffmpeg-2.0.1/android, which contains arm/lib and arm/include folders.

The arm/lib folder contains the shared libraries, while arm/include folder contains the header files for libavcodec, libavformat, libavfilter, libavutil, libswscale etc.

Note that the arm/lib folder contains both the library files (e.g.: libavcodec-55.so) and symbolic links (e.g.: libavcodec.so) to them. We can remove the symbolic links to avoid confusion.

5. Make ffmpeg Libraries available for Your Projects

Now we’ve compiled the ffmpeg libraries and ready to use them. Android NDK allows us to reuse a compiled module through the import-module build command.

The reason we built our ffmpeg source code under $NDK/sources folder is that NDK build system will search for directories under this path for external modules automatically. To declare the ffmpeg libraries as reusable modules, we’ll need to add a file named $NDK/sources/ffmpeg-2.0.1/android/arm/Android.mk with the following content,

LOCAL_PATH:= $(call my-dir)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavcodec

LOCAL_SRC_FILES:= lib/libavcodec-55.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavformat

LOCAL_SRC_FILES:= lib/libavformat-55.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libswscale

LOCAL_SRC_FILES:= lib/libswscale-2.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavutil

LOCAL_SRC_FILES:= lib/libavutil-52.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavfilter

LOCAL_SRC_FILES:= lib/libavfilter-3.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libwsresample

LOCAL_SRC_FILES:= lib/libswresample-0.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

Below is an example of how we can use the libraries in a Android project’s jni/Android.mk file,

LOCAL_PATH := $(call my-dir)

 

include $(CLEAR_VARS)

 

LOCAL_MODULE    := tutorial03

LOCAL_SRC_FILES := tutorial03.c

LOCAL_LDLIBS := -llog -ljnigraphics -lz -landroid

LOCAL_SHARED_LIBRARIES := libavformat libavcodec libswscale libavutil

 

include $(BUILD_SHARED_LIBRARY)

$(call import-module,ffmpeg-2.0.1/android/arm)

Note that we called import-module with the relative path to $NDK/sources for the build system to locate the reusable ffmpeg libraries.

For real examples to how to use the ffmpeg libraries in Android app, please refer to my github repo of android-ffmpeg-tutorial.

Android Animation Playback: Inline Animation with TextView

TextView allows us to attach and detach markup objects to a range of text using Spannable. We can use this to implement inline animation within TextView.

0. AnimatedImageSpan

We implement a AnimatedImageSpan, which extends DynamicDrawableSpan. The object will accept a set of image frames and keep track of the frame to display. A handler and a runnable are implemented to update the frame to display.

Note that AnimatedImageSpan doesn’t update the screen by itself. It calls AnimatedImageUpdateHandler.updateFrame to refresh the entire TextView, which subsequently redraw the AnimatedImageSpan with new image.

public class AnimatedImageSpan extends DynamicDrawableSpan {

    private AnimationAssetsSet mGifAssets;

    private int mCurrentFrameIdx;

    private Context mContext;

    

    private SimpleImageMemCache mImageCache;

    private AnimatedImageUpdateHandler mImageUpdater;

    

    private final Handler handler = new Handler();

    

    public AnimatedImageSpan(Context context) {

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

    

    public void setAnimationAssets(AnimationAssetsSet pGifAssets) {

        mGifAssets = pGifAssets;

    }

    

    private Runnable mRunnable;

    private int mPlaybackTimes;

    private boolean mPlaying;

    public void playGif(final AnimationSettings pGifSettings, AnimatedImageUpdateHandler pListener) {

        mPlaying = true;

        mImageUpdater = pListener;

        mPlaybackTimes = 0;

        mRunnable = new Runnable() {

            public void run() {

                mCurrentFrameIdx = (mCurrentFrameIdx + 1)%mGifAssets.getNumOfFrames();

//                Logger.d(this, "current frame " + mCurrentFrameIdx);

                handler.postDelayed(this, pGifSettings.mDelay);

                if (null != mImageUpdater) {

//                    Logger.d(this, "update frame using listener " + mImageUpdater.getId());

                    mImageUpdater.updateFrame();

                }

                if (mCurrentFrameIdx == mGifAssets.getNumOfFrames() - 1) {

                    if (pGifSettings.mPlaybackTimes == 0) {

                        //repeat forever

                    } else {

                        mPlaybackTimes++;

                        if (mPlaybackTimes == pGifSettings.mPlaybackTimes) {

                            stopRendering();

                        }

                    }

                }

            }

        };

        handler.post(mRunnable);

    }

    

    public boolean isPlaying() {

        return mPlaying;

    }

    

    public void stopRendering() {

        handler.removeCallbacks(mRunnable);

        mPlaying = false;

    }

    

    @Override

    public Drawable getDrawable() {

        Bitmap bitmap = mImageCache.loadBitmap(mContext, mGifAssets.getGifFramePath(mCurrentFrameIdx));

        BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), bitmap);

        int width = drawable.getIntrinsicWidth();

        int height = drawable.getIntrinsicHeight();

        drawable.setBounds(0, 0, width > 0 ? width : 0, height > 0 ? height : 0);

        return drawable;

    }

 

    @Override

    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {

//        Logger.d(this, "draw " + mCurrentFrameIdx);

        Drawable b = getDrawable();

        canvas.save();

 

        int transY = bottom - b.getBounds().bottom;

        if (mVerticalAlignment == ALIGN_BASELINE) {

            transY -= paint.getFontMetricsInt().descent;

        }

 

        canvas.translate(x, transY);

        b.draw(canvas);

        canvas.restore();

    }

}

1. Detect Click Events

We want the animation to start playing when the area is clicked, therefore detecting clicking is necessary. We’ll extend ClickableSpan as below.

private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

When the click event is detected, we start the animation playback, if it’s clicked while the animation is playing, we stop the playback.

2. MyTextView

We glue everything together in the MyTextView as below.

public class MyTextView extends TextView {

    private SpannableStringBuilder mSb = new SpannableStringBuilder();

    private String dummyText = "dummy " + System.currentTimeMillis();

    private Context mContext;

    private SimpleImageMemCache mImageCache;

    private ArrayList<AnimatedImageSpan> mAnimatedImages = new ArrayList<AnimatedImageSpan>();

 

    public MyTextView(Context context) {

        super(context);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs) {

        super(context, attrs);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs, int defStyle) {

        super(context, attrs, defStyle);

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

 

    @Override

    protected void onDetachedFromWindow() {

        super.onDetachedFromWindow();

        Log.d(this.getClass().getName(), "onDetachedFromWindow ");

        for (AnimatedImageSpan ais : mAnimatedImages) {

            Log.d(this.getClass().getName(), "animation playing " + ais.isPlaying());

            if (ais.isPlaying()) {

                ais.stopRendering();

            }

        }

        mAnimatedImages.clear();

        mSb.clearSpans();

        mSb.clear();

    }

    

    public void appendText(String pStr) {

        mSb.append(pStr);

    }

    

    public void appendAnimation(AnimationAssetsSet pAsset, AnimationSettings pSettings) {

        mSb.append(dummyText);

        AnimatedImageSpan ais = new AnimatedImageSpan(mContext);

        ais.setImageCache(mImageCache);

        ais.setAnimationAssets(pAsset);

        mSb.setSpan(ais, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        AnimationClickableSpan clickSpan = new AnimationClickableSpan(this, ais, pSettings);

        mSb.setSpan(clickSpan, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        mAnimatedImages.add(ais);

    }

    

    public void finishAddingContent() {

        this.setText(mSb);

        this.setMovementMethod(LinkMovementMethod.getInstance());

    }

    

    private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

}

3. The Complete Source Code

The complete source code can found at github repo.

Android Animation Playback: Frame Animation

Android provides AnimationDrawable object to create frame animation as a series of Drawable objects. The details can be found at Drawable Animation guideline at http://developer.android.com/guide/topics/graphics/drawable-animation.html.

This post discusses a different approach where we extends an ImageView and update the image displayed on the ImageView to render an animation.

0. Decode Animation Frame

Decoding frames is relatively computation intensive and therefore it’s better to do it in a separate thread. This can be implemented in code below.

class MyThread extends Thread {

    boolean mIsPlayingGif;

    AnimationSettings mGifSettings;

    MyThread(AnimationSettings pGifSettings) {

        mIsPlayingGif = true;

        mGifSettings = AnimationSettings.newCopy(pGifSettings);

    }

    @Override

    public void run() {

        int repetitionCounter = 0;

        do {

            for (int i = 0; i < mGifAssets.getNumOfFrames(); ++i) {

                if (!mIsPlayingGif) {

                    break;

                }

                Log.d(this.getName(), FrameAnimationView.this.getWidth()

                        + ":" + FrameAnimationView.this.getHeight());

                switch (mLoadMethod) {

                case ASSETS:

                    mTmpBitmap = mImageCache.loadBitmap(mContext, mGifAssets.getGifFramePath(i));

                    break;

                case FILES:

                    mTmpBitmap = mImageCache.loadBitmap(mGifFrames.get(i));

                    break;

                case RESOURCES:

                    //TODO

                    break;

                }

                mHandler.post(mUpdateResults);

                try {

                    Thread.sleep(mGifSettings.mDelay);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

            if(0 != mGifSettings.mPlaybackTimes) {

                repetitionCounter++;

            }

        } while (mIsPlayingGif && repetitionCounter <= mGifSettings.mPlaybackTimes);

    }

}

1. Update Frame

Update the ImageView is required to be done in UI thread. Therefore we need to post a Runnable to a Handler object. The runnable is as below.

final Runnable mUpdateResults = new Runnable() {

    public void run() {

        if (mTmpBitmap != null && !mTmpBitmap.isRecycled()) {

            FrameAnimationView.this.setImageBitmap(mTmpBitmap);

        }

    }

};

2. Test the Animation

To test the animation, we can declare FrameAnimationView in an XML layout as below.

<;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"

    android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    tools:context=".FrameAnimationActivity" >;

    <roman10.tutorial.frameanimation.FrameAnimationView

        android:id="@+id/gifPreviewOne"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_marginTop="100dp" />;

</RelativeLayout>

Suppose we have all frames of an animation under assets/1/ folder, to test the animation, we can code an activity as below.

public class FrameAnimationActivity extends Activity {

    private FrameAnimationView mFrameAniView;

    private SimpleImageMemCache mImageCache;

    private Context mContext;

    

    public int convertDpToPixel(int dp) {

        Resources r = mContext.getResources();

        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));

    }

    

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        mContext = this;

        setContentView(R.layout.activity_frame_animation);

        mFrameAniView = (FrameAnimationView) this.findViewById(R.id.gifPreviewOne);

        mImageCache = new SimpleImageMemCache(0.20f, convertDpToPixel(320), convertDpToPixel(200));

        mFrameAniView.setImageCache(mImageCache);

        mFrameAniView.setAnimationAssets(new AnimationAssetsSet(this, "1"));

    }

 

    @Override

    public void onResume() {

        super.onResume();

        mFrameAniView.playGif(new AnimationSettings());

    }

 

    @Override

    public void onPause() {

        super.onPause();

        mFrameAniView.stopRendering();

    }

    

    @Override

    public void onDestroy() {

        super.onDestroy();

        mFrameAniView.clearBitmap();

        mImageCache.clearCache();

    }

}

3. The Complete Source Code

The complete source code can be found at github repo. The code includes a simple image memory cache and a few bitmap utilities to optimize image decoding.

Android Animation Playback: Display GIF Animation in WebView

Android doesn’t support GIF natively. However, there’re a few different approaches to display GIF-like animation. In this and subsequently three blogs, we’ll cover two different approaches to display animation, including WebView and Frame Animation. We’ll start with GIF Animation playback in WebView.

The idea of using WebView to display GIF is simply embed the GIF into a few lines of HTML code as below.

<html>

    <head>

    <style type='text/css'>body{margin:auto auto;text-align:center;} img{width:100%25;} </style>

    </head>

    <body>

    <img src="1.gif" width="100%" />

    </body>

</html>

We can create a custom view which extends the WebView to display GIF animation as below.

public class GifWebView extends WebView {

    

    public GifWebView(Context context) {

        super(context);

    }

    

    public GifWebView(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

        

    public void setGifAssetPath(String pPath) {

        String baseUrl = pPath.substring(0, pPath.lastIndexOf("/") + 1);

        String fileName = pPath.substring(pPath.lastIndexOf("/")+1);

        StringBuilder strBuilder = new StringBuilder();

        strBuilder.append("<html><head><style type='text/css'>body{margin:auto auto;text-align:center;} img{width:100%25;} </style>");

        strBuilder.append("</head><body>");

        strBuilder.append("<img src="" + fileName + "" width="100%" /></body></html>");

        String data = strBuilder.toString();

        Log.d(this.getClass().getName(), "data: " + data);

        Log.d(this.getClass().getName(), "base url: " + baseUrl);

        Log.d(this.getClass().getName(), "file name: " + fileName);

        loadDataWithBaseURL(baseUrl, data, "text/html", "utf-8", null);

    }

}

We can declare a XML layout with the custom view.

<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"

    android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    tools:context=".GifWebviewDisplayActivity" >

 

    <roman10.tutorial.gifinwebview.GifWebView

        android:id="@+id/gif_view"

        android:layout_height="match_parent"

        android:layout_width="match_parent">

    </roman10.tutorial.gifinwebview.GifWebView>

 

</RelativeLayout>

Suppose we have an gif animation file 1.gif under assets folder, we can display the gif file in an activity as below.

public class GifWebviewDisplayActivity extends Activity {

    private GifWebView gifView;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_gif_webview_display);

        gifView = (GifWebView) findViewById(R.id.gif_view);

        gifView.setGifAssetPath("file:///android_asset/1.gif");

    }

}

The complete source code can be found at github repo.

References:

1. WebView Android doc: http://developer.android.com/reference/android/webkit/WebView.html

Android Property Animation — ValueAnimator

This is the second post on Android Property Animation. Previous post can be found below.

Android Property Animation Overview.

ValueAnimator is the core of the Property Animation system. It provides a timing engine which calculates the animation values. It also allows us to get notified at every animation frame through ValueAnimator.AnimatorUpdateListener interface.

Animation Handler

All animations created by Property Animation system share a single timing pulse, which is maintained by a custom static handler AnimationHandler at ValueAnimator class. Below is the code extracted from NineOldAndroids library ValueAnimator class.

/**

 * This custom, static handler handles the timing pulse that is shared by

 * all active animations. This approach ensures that the setting of animation

 * values will happen on the UI thread and that all animations will share

 * the same times for calculating their values, which makes synchronizing

 * animations possible.

 *

 */

private static class AnimationHandler extends Handler {

    /**

     * There are only two messages that we care about: ANIMATION_START and

     * ANIMATION_FRAME. The START message is sent when an animation's start()

     * method is called. It cannot start synchronously when start() is called

     * because the call may be on the wrong thread, and it would also not be

     * synchronized with other animations because it would not start on a common

     * timing pulse. So each animation sends a START message to the handler, which

     * causes the handler to place the animation on the active animations queue and

     * start processing frames for that animation.

     * The FRAME message is the one that is sent over and over while there are any

     * active animations to process.

     */

    @Override

    public void handleMessage(Message msg) {

        boolean callAgain = true;

        ArrayList<ValueAnimator> animations = sAnimations.get();

        ArrayList<ValueAnimator> delayedAnims = sDelayedAnims.get();

        switch (msg.what) {

            // TODO: should we avoid sending frame message when starting if we

            // were already running?

            case ANIMATION_START:

                ArrayList<ValueAnimator> pendingAnimations = sPendingAnimations.get();

                if (animations.size() > 0 || delayedAnims.size() > 0) {

                    callAgain = false;

                }

                // pendingAnims holds any animations that have requested to be started

                // We're going to clear sPendingAnimations, but starting animation may

                // cause more to be added to the pending list (for example, if one animation

                // starting triggers another starting). So we loop until sPendingAnimations

                // is empty.

                while (pendingAnimations.size() > 0) {

                    ArrayList<ValueAnimator> pendingCopy =

                            (ArrayList<ValueAnimator>) pendingAnimations.clone();

                    pendingAnimations.clear();

                    int count = pendingCopy.size();

                    for (int i = 0; i < count; ++i) {

                        ValueAnimator anim = pendingCopy.get(i);

                        // If the animation has a startDelay, place it on the delayed list

                        if (anim.mStartDelay == 0) {

                            anim.startAnimation();

                        } else {

                            delayedAnims.add(anim);

                        }

                    }

                }

                // fall through to process first frame of new animations

            case ANIMATION_FRAME:

                // currentTime holds the common time for all animations processed

                // during this frame

                long currentTime = AnimationUtils.currentAnimationTimeMillis();

                ArrayList<ValueAnimator> readyAnims = sReadyAnims.get();

                ArrayList<ValueAnimator> endingAnims = sEndingAnims.get();


                // First, process animations currently sitting on the delayed queue, adding

                // them to the active animations if they are ready

                int numDelayedAnims = delayedAnims.size();

                for (int i = 0; i < numDelayedAnims; ++i) {

                    ValueAnimator anim = delayedAnims.get(i);

                    if (anim.delayedAnimationFrame(currentTime)) {

                        readyAnims.add(anim);

                    }

                }

                int numReadyAnims = readyAnims.size();

                if (numReadyAnims > 0) {

                    for (int i = 0; i < numReadyAnims; ++i) {

                        ValueAnimator anim = readyAnims.get(i);

                        anim.startAnimation();

                        anim.mRunning = true;

                        delayedAnims.remove(anim);

                    }

                    readyAnims.clear();

                }


                // Now process all active animations. The return value from animationFrame()

                // tells the handler whether it should now be ended

                int numAnims = animations.size();

                int i = 0;

                while (i < numAnims) {

                    ValueAnimator anim = animations.get(i);

                    if (anim.animationFrame(currentTime)) {

                        endingAnims.add(anim);

                    }

                    if (animations.size() == numAnims) {

                        ++i;

                    } else {

                        // An animation might be canceled or ended by client code

                        // during the animation frame. Check to see if this happened by

                        // seeing whether the current index is the same as it was before

                        // calling animationFrame(). Another approach would be to copy

                        // animations to a temporary list and process that list instead,

                        // but that entails garbage and processing overhead that would

                        // be nice to avoid.

                        --numAnims;

                        endingAnims.remove(anim);

                    }

                }

                if (endingAnims.size() > 0) {

                    for (i = 0; i < endingAnims.size(); ++i) {

                        endingAnims.get(i).endAnimation();

                    }

                    endingAnims.clear();

                }


                // If there are still active or delayed animations, call the handler again

                // after the frameDelay

                if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) {

                    sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -

                        (AnimationUtils.currentAnimationTimeMillis() - currentTime)));

                }

                break;

        }

    }

}

The handler only handles two types of messages, including ANIMATION_START and ANIMATION_FRAME.

ANIMATION_START message is generated when an animation starts, the handler will receive the message and put the animation into delay queue. Next the handler decide if the animations from the delay queue should be started immediately (add to ready queue). The handler will then process all active animations, which includes those in the ready queue.

ANIMATION_FRAME message is sent by the handler itself to schedule next animation frame update. The handler checks if any animation from the delay queue should be started and then process all active animations. (The code to handle ANIMATION_FRAME is actually part of the code for handling ANIMATION_START).

The handler also checks if an animation should be ended or not in the handler.

By using this static handler, all animations are synchronized on a frame by frame basis.

Using ValueAnimator

Animating a single value with ValueAnimator is straightforward. ValueAnimator provides static method like ofInt and ofFloat to animation between integer and floating point values. We will skip that and discuss how to animation multiple values.

Suppose we need to animate an ImageView of a red square from bottom left to top right as shown in the screenshots below.

 

Figure 1. Animation Bottom Left to Top Right

It is obvious that we need to update both the x and y coordinates. The naive approach is to create two animations and play them simultaneously, but it is not efficient. We can use PropertyValuesHolder to combine multiple values. Alternatively, we can use custom objects as animation values. We’ll explore both approaches.

We will use the layout as indicated by the xml file below. We want to move the ImageView inside the FrameLayout with id “container”.

 

<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=".MainActivity" >


    <FrameLayout

        android:id="@+id/container"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:layout_alignParentTop="true">

    </FrameLayout>

    <LinearLayout

        android:id="@+id/controlBtns"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:orientation="horizontal"

        android:layout_alignParentBottom="true">

        <Button

            android:id="@+id/btnStart"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:text="Start">

        </Button>

        <Button

            android:id="@+id/btnReset"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:text="Reset">

        </Button>

    </LinearLayout>

    <ImageView

        android:id="@+id/image"

        android:layout_width="50dp"

        android:layout_height="50dp"

        android:src="#ffff4444"

        android:layout_above="@id/controlBtns">

    </ImageView>

</RelativeLayout>

Using PropertyValuesHolder

PropertyValuesHolder contains information about a property and its values during animation. It is used with ValueAnimator or ObjectAnimator to animate multiple properties at the same time.

In order to support property animation in pre-3.0 Android devices, NineOldAndroids provide a class AnimatorProxy to wrap a view and handles drawing of animated views. We need to wrap the ImageView in our code as below.

ImageView mImageView = (ImageView) this.findViewById(R.id.image);

mImageAnimatorProxy = AnimatorProxy.wrap(mImageView);

The code below creates an animation which animates two values which represent X and Y position of the image view with respective to the frame layout.

PropertyValuesHolder widthPropertyHolder = PropertyValuesHolder.ofFloat("posX", mImageAnimatorProxy.getX(), container.getWidth() - mImageView.getWidth());

PropertyValuesHolder heightPropertyHolder = PropertyValuesHolder.ofFloat("posY", mImageAnimatorProxy.getY(), 0);

ValueAnimator mTranslationAnimator = ValueAnimator.ofPropertyValuesHolder(widthPropertyHolder, heightPropertyHolder);

mTranslationAnimator.addUpdateListener(ValueAnimatorDemo.this);

mTranslationAnimator.setDuration(1000);

mTranslationAnimator.start();

We use two PropertyValuesHolder to animate two values named posX and posY. We then created the animation using ValueAnimator. We registered the listener, set the duration and start the animation. The listener interface is implemented by overriding the onAnimationUpdate callback function as below.

@Override

public void onAnimationUpdate(ValueAnimator arg0) {

    float posX = (Float) arg0.getAnimatedValue("posX");

    float posY = (Float) arg0.getAnimatedValue("posY");

    mImageAnimatorProxy.setX(posX);

    mImageAnimatorProxy.setY(posY);

}

We retrieve the animated values posX and posY at every animation frame and update it.

Using Custom Objects as Animation Values

ValueAnimator can be used to animate custom objects also. We’ll need to specify a TypeEvaluator to tell the ValueAnimator how the values should be calculated.

In our example, we are moving the position of the ImageView, so we can create a custom object to represent the position of the ImageView and animate it. The code below creates a class Position to represent the position of the ImageView.

private class Position {

    private float posX;

    private float posY;


    public float getPosX() {

        return posX;

    }


    public float getPosY() {

        return posY;

    }


    Position(float pPosX, float pPosY) {

        posX = pPosX;

        posY = pPosY;

    }

}

We also provide to TypeEvaluator by implementing the android.animation.TypeEvaluator<T> interface.

private class PositionTypeEvaluator implements TypeEvaluator<Position> {

    @Override

    public Position evaluate(float fraction, Position startValue, Position endValue) {

        float posX = startValue.getPosX() + (endValue.getPosX() - startValue.getPosX()) * fraction;

        float posY = startValue.getPosY() + (endValue.getPosY() - startValue.getPosY()) * fraction;

        return new Position(posX, posY);

    }

}

At every animation frame, the callback function evaluate will be triggered with three arguments, including the elapsed time so far, the animation start position and end position. We can calculate the current position should be based on the three input arguments and returns it.

With the custom object and TypeEvaluator in place, the animation can be easily created as below.

ValueAnimator mAnimator = ValueAnimator.ofObject(new PositionTypeEvaluator(), new Position(mOriX, mOriY),

                        new Position(container.getWidth() - mImageView.getWidth(), 0));

mAnimator.addUpdateListener(ValueAnimatorDemo2.this);

mAnimator.setDuration(1000);

mAnimator.start();

Of course, we still need to implement the ValueAnimator.AnimatorUpdateListener interface.

@Override

public void onAnimationUpdate(ValueAnimator pAnimator) {

    Position currentPos = (Position) pAnimator.getAnimatedValue();

    mImageAnimatorProxy.setX(currentPos.getPosX());

    mImageAnimatorProxy.setY(currentPos.getPosY());

}

Note that we can also use ViewPropertyAnimator, which I’ll cover in another post if I got time.

You can get the full source code here: https://github.com/roman10/roman10-android-tutorial/tree/master/PropertyAnimation

References:

Android ValueAnimator doc: http://developer.android.com/reference/android/animation/ValueAnimator.html