LayoutLib: Animation support in insert/move/removeChild actions.

Also update to use the new SceneResult API.

Change-Id: Iaac6df0c250fbefc8758310c37e0cf47cae6875d
diff --git a/bridge/src/com/android/layoutlib/bridge/Bridge.java b/bridge/src/com/android/layoutlib/bridge/Bridge.java
index 7aa0e3c..2a94774 100644
--- a/bridge/src/com/android/layoutlib/bridge/Bridge.java
+++ b/bridge/src/com/android/layoutlib/bridge/Bridge.java
@@ -335,7 +335,7 @@
                 t2 = t.getCause();
             }
             return new BridgeLayoutScene(null,
-                    new SceneResult(SceneStatus.ERROR_UNKNOWN, t2.getMessage(), t2));
+                    SceneStatus.ERROR_UNKNOWN.getResult(t2.getMessage(), t2));
         }
     }
 
diff --git a/bridge/src/com/android/layoutlib/bridge/impl/AnimationThread.java b/bridge/src/com/android/layoutlib/bridge/impl/AnimationThread.java
index 2b9d52f..400a05f 100644
--- a/bridge/src/com/android/layoutlib/bridge/impl/AnimationThread.java
+++ b/bridge/src/com/android/layoutlib/bridge/impl/AnimationThread.java
@@ -22,7 +22,6 @@
 import com.android.layoutlib.api.SceneResult.SceneStatus;
 import com.android.layoutlib.bridge.Bridge;
 
-import android.animation.Animator;
 import android.animation.ValueAnimator;
 import android.os.Handler;
 import android.os.Handler_Delegate;
@@ -32,7 +31,19 @@
 import java.util.LinkedList;
 import java.util.Queue;
 
-public class AnimationThread extends Thread {
+/**
+ * Abstract animation thread.
+ * <p/>
+ * This does not actually start an animation, instead it fakes a looper that will play whatever
+ * animation is sending messages to its own {@link Handler}.
+ * <p/>
+ * Classes should implement {@link #preAnimation()} and {@link #postAnimation()}.
+ * <p/>
+ * If {@link #preAnimation()} does not start an animation something then the thread doesn't do
+ * anything.
+ *
+ */
+public abstract class AnimationThread extends Thread {
 
     private static class MessageBundle {
         final Handler mTarget;
@@ -47,17 +58,19 @@
     }
 
     private final LayoutSceneImpl mScene;
-    private final Animator mAnimator;
 
     Queue<MessageBundle> mQueue = new LinkedList<MessageBundle>();
     private final IAnimationListener mListener;
 
-    public AnimationThread(LayoutSceneImpl scene, Animator animator, IAnimationListener listener) {
+    public AnimationThread(LayoutSceneImpl scene, String threadName, IAnimationListener listener) {
+        super(threadName);
         mScene = scene;
-        mAnimator = animator;
         mListener = listener;
     }
 
+    public abstract SceneResult preAnimation();
+    public abstract void postAnimation();
+
     @Override
     public void run() {
         Bridge.prepareThread();
@@ -73,13 +86,20 @@
                 }
             });
 
-            // start the animation. This will send a message to the handler right away, so
-            // mQueue is filled when this method returns.
-            mAnimator.start();
+            // call out to the pre-animation work, which should start an animation or more.
+            SceneResult result = preAnimation();
+            if (result.isSuccess() == false) {
+                mListener.done(result);
+            }
 
             // loop the animation
             LayoutScene scene = mScene.getScene();
             do {
+                // check early.
+                if (mListener.isCanceled()) {
+                    break;
+                }
+
                 // get the next message.
                 MessageBundle bundle = mQueue.poll();
                 if (bundle == null) {
@@ -97,8 +117,13 @@
                     }
                 }
 
+                // check after sleeping.
+                if (mListener.isCanceled()) {
+                    break;
+                }
+
                 // ready to do the work, acquire the scene.
-                SceneResult result = mScene.acquire(250);
+                result = mScene.acquire(250);
                 if (result.isSuccess() == false) {
                     mListener.done(result);
                     return;
@@ -107,6 +132,11 @@
                 // process the bundle. If the animation is not finished, this will enqueue
                 // the next message, so mQueue will have another one.
                 try {
+                    // check after acquiring in case it took a while.
+                    if (mListener.isCanceled()) {
+                        break;
+                    }
+
                     bundle.mTarget.handleMessage(bundle.mMessage);
                     if (mScene.render().isSuccess()) {
                         mListener.onNewFrame(scene);
@@ -114,10 +144,11 @@
                 } finally {
                     mScene.release();
                 }
-            } while (mQueue.size() > 0);
+            } while (mListener.isCanceled() == false && mQueue.size() > 0);
 
             mListener.done(SceneStatus.SUCCESS.getResult());
         } finally {
+            postAnimation();
             Handler_Delegate.setCallback(null);
             Bridge.cleanupThread();
         }
diff --git a/bridge/src/com/android/layoutlib/bridge/impl/LayoutSceneImpl.java b/bridge/src/com/android/layoutlib/bridge/impl/LayoutSceneImpl.java
index 3c8e8cd..359180f 100644
--- a/bridge/src/com/android/layoutlib/bridge/impl/LayoutSceneImpl.java
+++ b/bridge/src/com/android/layoutlib/bridge/impl/LayoutSceneImpl.java
@@ -43,8 +43,9 @@
 import com.android.layoutlib.bridge.android.BridgeWindowSession;
 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
 
+import android.animation.Animator;
 import android.animation.AnimatorInflater;
-import android.animation.ObjectAnimator;
+import android.animation.LayoutTransition;
 import android.app.Fragment_Delegate;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap_Delegate;
@@ -56,7 +57,6 @@
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewParent;
 import android.view.View.AttachInfo;
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup.LayoutParams;
@@ -346,7 +346,7 @@
 
             return SceneStatus.SUCCESS.getResult();
         } catch (PostInflateException e) {
-            return new SceneResult(SceneStatus.ERROR_INFLATION, e.getMessage(), e);
+            return SceneStatus.ERROR_INFLATION.getResult(e.getMessage(), e);
         } catch (Throwable e) {
             // get the real cause of the exception.
             Throwable t = e;
@@ -357,7 +357,7 @@
             // log it
             mParams.getLogger().error(t);
 
-            return new SceneResult(SceneStatus.ERROR_INFLATION, t.getMessage(), t);
+            return SceneStatus.ERROR_INFLATION.getResult(t.getMessage(), t);
         }
     }
 
@@ -370,13 +370,14 @@
      *      the scene, or if {@link #acquire(long)} was not called.
      *
      * @see SceneParams#getRenderingMode()
+     * @see LayoutScene#render(long)
      */
     public SceneResult render() {
         checkLock();
 
         try {
             if (mViewRoot == null) {
-                return new SceneResult(SceneStatus.ERROR_NOT_INFLATED);
+                return SceneStatus.ERROR_NOT_INFLATED.getResult();
             }
             // measure the views
             int w_spec, h_spec;
@@ -482,7 +483,7 @@
             // log it
             mParams.getLogger().error(t);
 
-            return new SceneResult(SceneStatus.ERROR_UNKNOWN, t.getMessage(), t);
+            return SceneStatus.ERROR_UNKNOWN.getResult(t.getMessage(), t);
         }
     }
 
@@ -493,6 +494,8 @@
      *
      * @throws IllegalStateException if the current context is different than the one owned by
      *      the scene, or if {@link #acquire(long)} was not called.
+     *
+     * @see LayoutScene#animate(Object, String, boolean, IAnimationListener)
      */
     public SceneResult animate(Object targetObject, String animationName,
             boolean isFrameworkAnimation, IAnimationListener listener) {
@@ -515,12 +518,11 @@
 
         if (animationResource != null) {
             try {
-                ObjectAnimator anim = (ObjectAnimator) AnimatorInflater.loadAnimator(
-                        mContext, animationId);
+                Animator anim = AnimatorInflater.loadAnimator(mContext, animationId);
                 if (anim != null) {
                     anim.setTarget(targetObject);
 
-                    new AnimationThread(this, anim, listener).start();
+                    new PlayAnimationThread(anim, this, animationName, listener).start();
 
                     return SceneStatus.SUCCESS.getResult();
                 }
@@ -531,15 +533,25 @@
                     t = t.getCause();
                 }
 
-                return new SceneResult(SceneStatus.ERROR_UNKNOWN, t.getMessage(), t);
+                return SceneStatus.ERROR_UNKNOWN.getResult(t.getMessage(), t);
             }
         }
 
-        return new SceneResult(SceneStatus.ERROR_ANIM_NOT_FOUND);
+        return SceneStatus.ERROR_ANIM_NOT_FOUND.getResult();
     }
 
-    public SceneResult insertChild(ViewGroup parentView, IXmlPullParser childXml,
-            int index, IAnimationListener listener) {
+    /**
+     * Insert a new child into an existing parent.
+     * <p>
+     * {@link #acquire(long)} must have been called before this.
+     *
+     * @throws IllegalStateException if the current context is different than the one owned by
+     *      the scene, or if {@link #acquire(long)} was not called.
+     *
+     * @see LayoutScene#insertChild(Object, IXmlPullParser, int, IAnimationListener)
+     */
+    public SceneResult insertChild(final ViewGroup parentView, IXmlPullParser childXml,
+            final int index, IAnimationListener listener) {
         checkLock();
 
         // create a block parser for the XML
@@ -549,84 +561,217 @@
         // inflate the child without adding it to the root since we want to control where it'll
         // get added. We do pass the parentView however to ensure that the layoutParams will
         // be created correctly.
-        View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
-
-        // add it to the parentView in the correct location
-        try {
-            parentView.addView(child, index);
-        } catch (UnsupportedOperationException e) {
-            // looks like this is a view class that doesn't support children manipulation!
-            return new SceneResult(SceneStatus.ERROR_VIEWGROUP_NO_CHILDREN);
-        }
+        final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
 
         invalidateRenderingSize();
 
-        SceneResult result = render();
+        if (listener != null) {
+            new AnimationThread(this, "insertChild", listener) {
+
+                @Override
+                public SceneResult preAnimation() {
+                    parentView.setLayoutTransition(new LayoutTransition());
+                    return addView(parentView, child, index);
+                }
+
+                @Override
+                public void postAnimation() {
+                    parentView.setLayoutTransition(null);
+                }
+            }.start();
+
+            // always return success since the real status will come through the listener.
+            return SceneStatus.SUCCESS.getResult(child);
+        }
+
+        // add it to the parentView in the correct location
+        SceneResult result = addView(parentView, child, index);
+        if (result.isSuccess() == false) {
+            return result;
+        }
+
+        result = render();
         if (result.isSuccess()) {
-            result.setData(child);
+            result = result.getCopyWithData(child);
         }
 
         return result;
     }
 
-    public SceneResult moveChild(ViewGroup parentView, View childView, int index,
+    /**
+     * Adds a given view to a given parent at a given index.
+     *
+     * @param parent the parent to receive the view
+     * @param view the view to add to the parent
+     * @param index the index where to do the add.
+     *
+     * @return a SceneResult with {@link SceneStatus#SUCCESS} or
+     *     {@link SceneStatus#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
+     *     adding views.
+     */
+    private SceneResult addView(ViewGroup parent, View view, int index) {
+        try {
+            parent.addView(view, index);
+            return SceneStatus.SUCCESS.getResult();
+        } catch (UnsupportedOperationException e) {
+            // looks like this is a view class that doesn't support children manipulation!
+            return SceneStatus.ERROR_VIEWGROUP_NO_CHILDREN.getResult();
+        }
+    }
+
+    /**
+     * Moves a view to a new parent at a given location
+     * <p>
+     * {@link #acquire(long)} must have been called before this.
+     *
+     * @throws IllegalStateException if the current context is different than the one owned by
+     *      the scene, or if {@link #acquire(long)} was not called.
+     *
+     * @see LayoutScene#moveChild(Object, Object, int, Map, IAnimationListener)
+     */
+    public SceneResult moveChild(final ViewGroup parentView, final View childView, final int index,
             Map<String, String> layoutParamsMap, IAnimationListener listener) {
         checkLock();
 
-        LayoutParams layoutParams = null;
-        try {
-            ViewParent parent = childView.getParent();
-            if (parent instanceof ViewGroup) {
-                ViewGroup parentGroup = (ViewGroup) parent;
-                parentGroup.removeView(childView);
-            }
-
-            // add it to the parentView in the correct location
-
-            if (layoutParamsMap != null) {
-                // need to create a new LayoutParams object for the new parent.
-                layoutParams = parentView.generateLayoutParams(
-                        new BridgeLayoutParamsMapAttributes(layoutParamsMap));
-
-                parentView.addView(childView, index, layoutParams);
-            } else {
-                parentView.addView(childView, index);
-            }
-        } catch (UnsupportedOperationException e) {
-            // looks like this is a view class that doesn't support children manipulation!
-            return new SceneResult(SceneStatus.ERROR_VIEWGROUP_NO_CHILDREN);
-        }
-
         invalidateRenderingSize();
 
-        SceneResult result = render();
+        LayoutParams layoutParams = null;
+        if (layoutParamsMap != null) {
+            // need to create a new LayoutParams object for the new parent.
+            layoutParams = parentView.generateLayoutParams(
+                    new BridgeLayoutParamsMapAttributes(layoutParamsMap));
+        }
+
+        if (listener != null) {
+            final LayoutParams params = layoutParams;
+            new AnimationThread(this, "moveChild", listener) {
+
+                @Override
+                public SceneResult preAnimation() {
+                    parentView.setLayoutTransition(new LayoutTransition());
+                    return moveView(parentView, childView, index, params);
+                }
+
+                @Override
+                public void postAnimation() {
+                    parentView.setLayoutTransition(null);
+                }
+            }.start();
+
+            // always return success since the real status will come through the listener.
+            return SceneStatus.SUCCESS.getResult(layoutParams);
+        }
+
+        SceneResult result = moveView(parentView, childView, index, layoutParams);
+        if (result.isSuccess() == false) {
+            return result;
+        }
+
+        result = render();
         if (layoutParams != null && result.isSuccess()) {
-            result.setData(layoutParams);
+            result = result.getCopyWithData(layoutParams);
         }
 
         return result;
     }
 
-    public SceneResult removeChild(View childView, IAnimationListener listener) {
-        checkLock();
-
+    /**
+     * Moves a View from its current parent to a new given parent at a new given location, with
+     * an optional new {@link LayoutParams} instance
+     *
+     * @param parent the new parent
+     * @param view the view to move
+     * @param index the new location in the new parent
+     * @param params an option (can be null) {@link LayoutParams} instance.
+     *
+     * @return a SceneResult with {@link SceneStatus#SUCCESS} or
+     *     {@link SceneStatus#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
+     *     adding views.
+     */
+    private SceneResult moveView(ViewGroup parent, View view, int index, LayoutParams params) {
         try {
-            ViewParent parent = childView.getParent();
-            if (parent instanceof ViewGroup) {
-                ViewGroup parentGroup = (ViewGroup) parent;
-                parentGroup.removeView(childView);
+            ViewGroup previousParent = (ViewGroup) view.getParent();
+            previousParent.removeView(view);
+
+            // add it to the parentView in the correct location
+
+            if (params != null) {
+                parent.addView(view, index, params);
+            } else {
+                parent.addView(view, index);
             }
+
+            return SceneStatus.SUCCESS.getResult();
         } catch (UnsupportedOperationException e) {
             // looks like this is a view class that doesn't support children manipulation!
-            return new SceneResult(SceneStatus.ERROR_VIEWGROUP_NO_CHILDREN);
+            return SceneStatus.ERROR_VIEWGROUP_NO_CHILDREN.getResult();
         }
+    }
+
+    /**
+     * Removes a child from its current parent.
+     * <p>
+     * {@link #acquire(long)} must have been called before this.
+     *
+     * @throws IllegalStateException if the current context is different than the one owned by
+     *      the scene, or if {@link #acquire(long)} was not called.
+     *
+     * @see LayoutScene#removeChild(Object, IAnimationListener)
+     */
+    public SceneResult removeChild(final View childView, IAnimationListener listener) {
+        checkLock();
 
         invalidateRenderingSize();
 
+        final ViewGroup parent = (ViewGroup) childView.getParent();
+
+        if (listener != null) {
+            new AnimationThread(this, "moveChild", listener) {
+
+                @Override
+                public SceneResult preAnimation() {
+                    parent.setLayoutTransition(new LayoutTransition());
+                    return removeView(parent, childView);
+                }
+
+                @Override
+                public void postAnimation() {
+                    parent.setLayoutTransition(null);
+                }
+            }.start();
+
+            // always return success since the real status will come through the listener.
+            return SceneStatus.SUCCESS.getResult();
+        }
+
+        SceneResult result = removeView(parent, childView);
+        if (result.isSuccess() == false) {
+            return result;
+        }
+
         return render();
     }
 
     /**
+     * Removes a given view from its current parent.
+     *
+     * @param view the view to remove from its parent
+     *
+     * @return a SceneResult with {@link SceneStatus#SUCCESS} or
+     *     {@link SceneStatus#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
+     *     adding views.
+     */
+    private SceneResult removeView(ViewGroup parent, View view) {
+        try {
+            parent.removeView(view);
+            return SceneStatus.SUCCESS.getResult();
+        } catch (UnsupportedOperationException e) {
+            // looks like this is a view class that doesn't support children manipulation!
+            return SceneStatus.ERROR_VIEWGROUP_NO_CHILDREN.getResult();
+        }
+    }
+
+    /**
      * Checks that the lock is owned by the current thread and that the current context is the one
      * from this scene.
      *
diff --git a/bridge/src/com/android/layoutlib/bridge/impl/PlayAnimationThread.java b/bridge/src/com/android/layoutlib/bridge/impl/PlayAnimationThread.java
new file mode 100644
index 0000000..bbc7f4b
--- /dev/null
+++ b/bridge/src/com/android/layoutlib/bridge/impl/PlayAnimationThread.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.layoutlib.bridge.impl;
+
+import com.android.layoutlib.api.SceneResult;
+import com.android.layoutlib.api.LayoutScene.IAnimationListener;
+import com.android.layoutlib.api.SceneResult.SceneStatus;
+
+import android.animation.Animator;
+
+public class PlayAnimationThread extends AnimationThread {
+
+    private final Animator mAnimator;
+
+    public PlayAnimationThread(Animator animator, LayoutSceneImpl scene, String animName,
+            IAnimationListener listener) {
+        super(scene, animName, listener);
+        mAnimator = animator;
+    }
+
+    @Override
+    public SceneResult preAnimation() {
+        // start the animation. This will send a message to the handler right away, so
+        // the queue is filled when this method returns.
+        mAnimator.start();
+
+        return SceneStatus.SUCCESS.getResult();
+    }
+
+    @Override
+    public void postAnimation() {
+        // nothing to be done.
+    }
+}