CannyViewAnimator — our own ViewAnimator

This article was first published here.

Hello, everybody! I really enjoy working with animations, to the point where I often feel like adding a couple of them into an Android project I participate in, or even into an app I just happen to see. Not long ago, in April 2016, my entry about the Animation class type started the Live Typing blog, and later I held a presentation about animations at an IT-related event in Omsk. In this article I’d like to tell you about our CannyViewAnimator library and how it was developed. This library was created for prettier View visibility switching. If you are interested in the library or the story of its development, or are at least intrigued by the issues I had to face and solve in the process, read on!

What is it all about?

First of all, imagine a situation typical for Android development. You have a screen with a list that’s retrieved from the server. While your precious data is being downloaded from your precious server, you display a loader; after the data has arrived, you check it and display it, or show a placeholder if it’s empty.

What would be the best solution for UI? Initially, we at Live Typing used the following solution we borrowed from U2020 and later transferred to our U2020 MVP: BetterViewAnimator — View that’s inherited from ViewAnimator. The only (but essential) difference of BetterViewAnimator from its parent is its ability to work with resource id. However, it’s not perfect.

What is ViewAnimator?

What is ViewAnimator?

ViewAnimator is a View inherited from FrameLayout that has only one of its children visible at a certain moment. There is a selection of methods for switching the visible child.

A significant drawback of BetterViewAnimator is that it’s only able to work with the obsolete AnimationFramework. In this case, CannyViewAnimator is a lifesaver as it’s capable of working with Animator and AppCompat Transition.

Github project link

How it began

One of these days, when I was making yet another list/loader/placeholder screen, I realized that even though we did use BetterViewAnimator, we didn’t take advantage of what was supposed to be its primary feature: animations. Feeling ambitious, I decided to add animation and, to my disappointment, soon realized that ViewAnimator only works with Animation. Searching for an alternative on Github yielded no suitable solutions. The only thing I found was the utterly inflexible Android View Controller that supported only eight pre-set animations. This could only mean one thing: time for some do-it-yourself development.

What I wanted to get

First of all, I thought over everything I wanted as a result:

  • the ability to control child visibility (as before);
  • the ability to use Animator and especially CircularRevealAnimator;
  • the ability to launch animations both sequentially and simultaneously (as ViewAnimator can only launch them sequentially);
  • the ability to use Transition;
  • a kit of standard animations that can be set through XML;
  • flexibility; the ability to set a separate animation for a separate child.

Having decided on the things I wanted, I started to plan the “architecture” of the future project, which would include three parts:

  • ViewAnimator would be responsible for child visibility switching;
  • TransitionViewAnimator would be inherited from ViewAnimator and used for working with Transition;
  • CannyViewAnimator would be inherited from TransitionViewAnimator and be responsible for working with Animator.

Animators and Transition setting was to be implemented via an interface with two parameters: a child that would fade in and a child that would fade out. Every time the visible child was changed, the necessary animation would be taken from the interface implementation. Three interfaces were planned:

  • InAnimator — responsible for the Animator of the appearing (fading in) child;
  • OutAnimator — responsible for the Animator of the disappearing (fading out) child;
  • CannyTransition — responsible for Transition.

I decided to create a single interface for Transition because Transition is overlaid on all appearing and disappearing children at once. The concept looked reasonable and I got to work.

ViewAnimator

As I didn’t feel like reinventing the wheel, my base class was a copycat from SDK’s ViewAnimator, with Animation support removed and methods optimized (as I considered a number of them redundant). I also remembered to add methods from BetterViewAnimator. The final list of the essential methods was as follows:

  •  void setDisplayedChildIndex(int inChildIndex) — displays the child with the specified index;
  • void setDisplayedChildId(@IdRes int id) — displays the child with the specified id;
  • void setDisplayedChild(View view) — displays a specific child;
  • int getDisplayedChildIndex() — gets the index of the displayed child;
  • View getDisplayedChild() — gets the displayed child;
  • int getDisplayedChildId() — gets the id of the displayed child.    

After some thought, I decided to additionally save the position of the current visible child in onSaveInstanceState() and restore it onRestoreInstanceState(Parcelable state), displaying it immediately. The actual code looks like this:

public class ViewAnimator extends FrameLayout {
    private int lastWhichIndex = 0;
    public ViewAnimator(Context context) {
        super(context);
    }
    public ViewAnimator(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public void setDisplayedChildIndex(int inChildIndex) {
        if (inChildIndex >= getChildCount()) {
            inChildIndex = 0;
        } else if (inChildIndex < 0) {
            inChildIndex = getChildCount() - 1;
        }
        boolean hasFocus = getFocusedChild() != null;
        int outChildIndex = lastWhichIndex;
        lastWhichIndex = inChildIndex;
        changeVisibility(getChildAt(inChildIndex), getChildAt(outChildIndex));
        if (hasFocus) {
            requestFocus(FOCUS_FORWARD);
        }
    }
    public void setDisplayedChildId(@IdRes int id) {
        if (getDisplayedChildId() == id) {
            return;
        }
        for (int i = 0, count = getChildCount(); i < count; i++) {
            if (getChildAt(i).getId() == id) {
                setDisplayedChildIndex(i);
                return;
            }
        }
        throw new IllegalArgumentException("No view with ID " + id);
    }
    public void setDisplayedChild(View view) {
        setDisplayedChildId(view.getId());
    }
    public int getDisplayedChildIndex() {
        return lastWhichIndex;
    }
    public View getDisplayedChild() {
        return getChildAt(lastWhichIndex);
    }
    public int getDisplayedChildId() {
        return getDisplayedChild().getId();
    }
    protected void changeVisibility(View inChild, View outChild) {
        outChild.setVisibility(INVISIBLE);
        inChild.setVisibility(VISIBLE);
    }
    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
        if (getChildCount() == 1) {
            child.setVisibility(View.VISIBLE);
        } else {
            child.setVisibility(View.INVISIBLE);
        }
        if (index >= 0 && lastWhichIndex >= index) {
            setDisplayedChildIndex(lastWhichIndex + 1);
        }
    }
    @Override
    public void removeAllViews() {
        super.removeAllViews();
        lastWhichIndex = 0;
    }
    @Override
    public void removeView(View view) {
        final int index = indexOfChild(view);
        if (index >= 0) {
            removeViewAt(index);
        }
    }
    @Override
    public void removeViewAt(int index) {
        super.removeViewAt(index);
        final int childCount = getChildCount();
        if (childCount == 0) {
            lastWhichIndex = 0;
        } else if (lastWhichIndex >= childCount) {
            setDisplayedChildIndex(childCount - 1);
        } else if (lastWhichIndex == index) {
            setDisplayedChildIndex(lastWhichIndex);
        }
    }
    @Override
    public void removeViewInLayout(View view) {
        removeView(view);
    }
    @Override
    public void removeViews(int start, int count) {
        super.removeViews(start, count);
        if (getChildCount() == 0) {
            lastWhichIndex = 0;
        } else if (lastWhichIndex >= start && lastWhichIndex < start + count) {
            setDisplayedChildIndex(lastWhichIndex);
        }
    }
    @Override
    public void removeViewsInLayout(int start, int count) {
        removeViews(start, count);
    }
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        lastWhichIndex = ss.lastWhichIndex;
        setDisplayedChildIndex(lastWhichIndex);
    }
    @Override
    protected Parcelable onSaveInstanceState() {
        SavedState savedState = new SavedState(super.onSaveInstanceState());
        savedState.lastWhichIndex = lastWhichIndex;
        return savedState;
    }
    public static class SavedState extends View.BaseSavedState {
        int lastWhichIndex;
        SavedState(Parcelable superState) {
            super(superState);
        }
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(this.lastWhichIndex);
        }
        @Override
        public String toString() {
            return "ViewAnimator.SavedState{" +
                    "lastWhichIndex=" + lastWhichIndex +
                    '}';
        }
        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel source) {
                return new SavedState(source);
            }
            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
        protected SavedState(Parcel in) {
            super(in);
            this.lastWhichIndex = in.readInt();
        }
    }
}

Github link

TransitionViewAnimator

Having finished my work with ViewAnimator, I set out to solve the rather simple, but intriguing task of implementing Transition support. The idea was for animation to be prepared on overridden changeVisibility (View inChild, View outChild) method call. Transition is taken from the set CannyTransition and written in the class field with the help of the interface.

public interface CannyTransition {
    Transition getTransition(View inChild, View outChild);
}

Then, this Transition is launched in a separate method (I decided to make the launch a separate method with possible future issues in mind). Transition launch is done using the TransitionManager.beginDelayedTransition method which imposes some limitations, as Transition will be done only for those Views that have changed their properties in a certain amount of time after TransitionManager.beginDelayedTransition call. As implementation of Animators (and Animators can go on for quite a while) is planned in the future, TransitionManager.beginDelayedTransition has to be called right before Visibility is changed. Then, I call super.changeVisibility(inChild, outChild); that changes Visibility of the required children.

public class TransitionViewAnimator extends ViewAnimator {
    private CannyTransition cannyTransition;
    private Transition transition;
    public TransitionViewAnimator(Context context) {
        super(context);
    }
    public TransitionViewAnimator(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void changeVisibility(View inChild, View outChild) {
        prepareTransition(inChild, outChild);
        startTransition();
        super.changeVisibility(inChild, outChild);
    }
    protected void prepareTransition(View inChild, View outChild) {
        if (cannyTransition != null) {
            transition = cannyTransition.getTransition(inChild, outChild);
        }
    }
    public void startTransition() {
        if (transition != null) {
            TransitionManager.beginDelayedTransition(this, transition);
        }
    }
    public void setCannyTransition(CannyTransition cannyTransition) {
        this.cannyTransition = cannyTransition;
    }
}

Github link

CannyViewAnimator

Finally, it’s time to take a look at the most important layer. Initially, I wanted to use LayoutTransition for Animator management, but my dreams were crushed as it was impossible to use it to execute simultaneous animations without implementing crutches. There were also other issues caused by LayoutTransition’s shortcomings like the necessity to specify duration for AnimatorSet, lack of manual interruption and so on. I decided to write my own work logic. It looked very simple: launch Animator for a disappearing child and set Visibility.GONE for it at the end, and instantly make the appearing child visible and launch Animator for it.

I stumbled upon my first block here: it was impossible to launch Animator for a non-attached View (i.e., a View that hasn’t had onAttach executed or had onDetach executed). This would prevent me from changing the visibility of any child in a constructor or any other method that fires before onAttach. Anticipating a lot of various situations where it might come into play (and numerous issues on Github), I tried to fix the situation. Unfortunately, the easiest solution of calling the isAttachedToWindow() method wasn’t perfect as the method couldn’t be called if API version was lower than 19, and I really wanted to have 14 or higher API support.

However, View also has OnAttachStateChangeListener which I eagerly used. I redefined the void addView(View child, int index, ViewGroup.LayoutParams params) method and hooked this Listener up on every View. Then, I placed a link to the View itself and a Boolean variable that designates its state into HashMap. If onViewAttachedToWindow(View v) fired, I would set the variable to true, and if it was onViewDetachedFromWindow(View v), it would be set to false. Now, before Animator would launch, I could check the View state and decide whether I even needed to launch Animator or not.

After overcoming the first hurdle, I created two interfaces for getting Animators: InAnimator and OutAnimator.


public interface InAnimator {
    Animator getInAnimator(View inChild, View outChild);
}
public interface OutAnimator {
    Animator getOutAnimator(View inChild, View outChild);
}


Things went smoothly until I ran into the second roadblock: View state had to be restored after Animator was executed.

I found no solution on StackOverflow, and after a half an hour of brainstorming I decided to use ValueAnimator’s reverse method, setting its duration to zero.


if (animator instanceof ValueAnimator) {
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        animation.removeListener(this);
                        animation.setDuration(0);
                        ((ValueAnimator) animation).reverse();
                    }
                });
            }


This worked, and I actually posted my solution on StackOverflow.

Another issue came up right after:

CircularRevealAnimator wouldn’t execute its animation if View didn’t have its onMeasure executed yet.

This was bad news as ViewAnimator’s invisible children have Visibility.GONE, meaning they aren’t measured up to the moment when they get set another visibility type: VISIBLE or INVISIBLE. Even if I changed Visibility to INVISIBLE before animation started, it wouldn’t solve the problem. As View size measurement is done on frame render (and frame render is done asynchronously), there was no guarantee that by the moment Animator started View would be measured. I was averse to the idea of setting a delay or using onPreDrawListener, so I decided to use Visibility.INVISIBLE by default instead of Visibility.GONE.

My thoughts were racing with horror movie type scenes where my Views were measured on inflate (even though they don’t need to) and cause visual lag, so I decided to give it a test. I measured inflate time with Visibility.INVISIBLE and Visibility.GONE with 10 View and 5 nesting. It turned out that the difference was no more than 1ms. Maybe the phones became way more powerful than I remembered, or Android is so well-optimized nowadays, but I could vaguely recall that an unwanted Visibility.INVISIBLE used to lower performance significantly. In any case, problem solved

Barely coming to senses from my previous challenge, I went into the fray again. Because in FrameLayout children are placed on top of each other, simultaneous InAnimator and OutAnimator execution causes the animation to look different depending on the child’s index. All these annoyances that Animator implementation entailed made me feel like giving up, but it was a matter of principle now, so I carried on. The above problem would show up when I tried to make visible a View that was placed under the currently displayed View. As a result, the disappearing animation completely occluded the appearing animation and vice-versa. Looking for a solution, I tried to use other ViewGroup, fiddled with Z property and more.

Finally, I got an idea. I would just delete the necessary View from the container at the start of the animation, add it on top, and delete it at the end of the animation and then put it back in its initial place. It worked, but the animation would slightly lag on weaker devices, because adding or deleting View caused both it and its parent to call requestLayout() that calculated and rendered them again, so I had to dig into the ViewGroup class as well. After a few minutes, I understood that the order in which Views are placed in a ViewGroup depends only on one array, and then ViewGroup heirs (e.g., FrameLayout or LinearLayout) decide how to display it. Unfortunately, the array (as well as its work methods) was marked as private. There was a bright side to it, though: it wasn’t an issue in Java thanks to Java Reflection that allowed me to use methods to work with the array and directly manage the position of the required View. The method looked like this:


public void bringChildToPosition(View child, int position) {
        final int index = indexOfChild(child);
        if (index < 0 && position >= getChildCount()) return;
        try {
            Method removeFromArray = ViewGroup.class.getDeclaredMethod("removeFromArray", int.class);
            removeFromArray.setAccessible(true);
            removeFromArray.invoke(this, index);
            Method addInArray = ViewGroup.class.getDeclaredMethod("addInArray", View.class, int.class);
            addInArray.setAccessible(true);
            addInArray.invoke(this, child, position);
            Field mParent = View.class.getDeclaredField("mParent");
            mParent.setAccessible(true);
            mParent.set(child, this);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }


This method sets the position I require for the View. There is no need to call re-rendering at the end of all this magic, the animation will do it on its own. Now I could put the View I needed on top before the animation started and put it back when the animation ended.

This concludes the main part of my story about CannyViewAnimator.


public class CannyViewAnimator extends TransitionViewAnimator {
    public static final int SEQUENTIALLY = 1;
    public static final int TOGETHER = 2;
    private int animateType = SEQUENTIALLY;
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({SEQUENTIALLY, TOGETHER})
    @interface AnimateType {
    }
    public static final int FOR_POSITION = 1;
    public static final int IN_ALWAYS_TOP = 2;
    public static final int OUT_ALWAYS_TOP = 3;
    private int locationType = FOR_POSITION;
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({FOR_POSITION, IN_ALWAYS_TOP, OUT_ALWAYS_TOP})
    @interface LocationType {
    }
    private List<? extends InAnimator> inAnimator;
    private List<? extends OutAnimator> outAnimator;
    private final Map<View, Boolean> attachedList = new HashMap<>(getChildCount());
    public CannyViewAnimator(Context context) {
        super(context);
    }
    public CannyViewAnimator(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @SafeVarargs
    public final <T extends InAnimator> void setInAnimator(T... inAnimators) {
        setInAnimator(Arrays.asList(inAnimators));
    }
    public void setInAnimator(List<? extends InAnimator> inAnimators) {
        this.inAnimator = inAnimators;
    }
    @SafeVarargs
    public final <T extends OutAnimator> void setOutAnimator(T... outAnimators) {
        setOutAnimator(Arrays.asList(outAnimators));
    }
    public void setOutAnimator(List<? extends OutAnimator> outAnimators) {
        this.outAnimator = outAnimators;
    }
    @Override
    protected void changeVisibility(View inChild, View outChild) {
        if (attachedList.get(outChild) && attachedList.get(inChild)) {
            AnimatorSet animatorSet = new AnimatorSet();
            Animator inAnimator = mergeInAnimators(inChild, outChild);
            Animator outAnimator = mergeOutAnimators(inChild, outChild);
            prepareTransition(inChild, outChild);
            switch (animateType) {
                case SEQUENTIALLY:
                    animatorSet.playSequentially(outAnimator, inAnimator);
                    break;
                case TOGETHER:
                    animatorSet.playTogether(outAnimator, inAnimator);
                    break;
            }
            switch (locationType) {
                case FOR_POSITION:
                    addOnStartVisibleListener(inAnimator, inChild);
                    addOnEndInvisibleListener(outAnimator, outChild);
                    break;
                case IN_ALWAYS_TOP:
                    addOnStartVisibleListener(inAnimator, inChild);
                    addOnEndInvisibleListener(inAnimator, outChild);
                    addOnStartToTopOnEndToInitPositionListener(inAnimator, inChild);
                    break;
                case OUT_ALWAYS_TOP:
                    addOnStartVisibleListener(outAnimator, inChild);
                    addOnEndInvisibleListener(outAnimator, outChild);
                    addOnStartToTopOnEndToInitPositionListener(outAnimator, outChild);
                    break;
            }
            animatorSet.start();
        } else {
            super.changeVisibility(inChild, outChild);
        }
    }
    private AnimatorSet mergeInAnimators(final View inChild, final View outChild) {
        AnimatorSet animatorSet = new AnimatorSet();
        List<Animator> animators = new ArrayList<>(inAnimator.size());
        for (InAnimator inAnimator : this.inAnimator) {
            if (inAnimator != null) {
                Animator animator = inAnimator.getInAnimator(inChild, outChild);
                if (animator != null) {
                    animators.add(animator);
                }
            }
        }
        animatorSet.playTogether(animators);
        return animatorSet;
    }
    private AnimatorSet mergeOutAnimators(final View inChild, final View outChild) {
        AnimatorSet animatorSet = new AnimatorSet();
        List<Animator> animators = new ArrayList<>(outAnimator.size());
        for (OutAnimator outAnimator : this.outAnimator) {
            if (outAnimator != null) {
                Animator animator = outAnimator.getOutAnimator(inChild, outChild);
                if (animator != null)
                    animators.add(animator);
            }
        }
        animatorSet.playTogether(animators);
        addRestoreInitValuesListener(animatorSet);
        return animatorSet;
    }
    private void addRestoreInitValuesListener(AnimatorSet animatorSet) {
        for (Animator animator : animatorSet.getChildAnimations()) {
            if (animator instanceof ValueAnimator) {
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        animation.removeListener(this);
                        animation.setDuration(0);
                        ((ValueAnimator) animation).reverse();
                    }
                });
            }
        }
    }
    private void addOnStartVisibleListener(Animator animator, final View view) {
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                startTransition();
                view.setVisibility(VISIBLE);
            }
        });
    }
    private void addOnEndInvisibleListener(Animator animator, final View view) {
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startTransition();
                view.setVisibility(INVISIBLE);
            }
        });
    }
    private void addOnStartToTopOnEndToInitPositionListener(Animator animator, final View view) {
        final int initLocation = indexOfChild(view);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                bringChildToPosition(view, getChildCount() - 1);
            }
            @Override
            public void onAnimationEnd(Animator animation) {
                bringChildToPosition(view, initLocation);
            }
        });
    }
    public int getAnimateType() {
        return animateType;
    }
    public void setAnimateType(@AnimateType int animateType) {
        this.animateType = animateType;
    }
    public int getLocationType() {
        return locationType;
    }
    public void setLocationType(@LocationType int locationType) {
        this.locationType = locationType;
    }
    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        attachedList.put(child, false);
        child.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {
                attachedList.put(v, true);
            }
            @Override
            public void onViewDetachedFromWindow(View v) {
                attachedList.put(v, false);
            }
        });
        super.addView(child, index, params);
    }
    @Override
    public void removeAllViews() {
        attachedList.clear();
        super.removeAllViews();
    }
    @Override
    public void removeView(View view) {
        attachedList.remove(view);
        super.removeView(view);
    }
    @Override
    public void removeViewAt(int index) {
        attachedList.remove(getChildAt(index));
        super.removeViewAt(index);
    }
    @Override
    public void removeViews(int start, int count) {
        for (int i = start; i < start + count; i++) {
            attachedList.remove(getChildAt(i));
        }
        super.removeViews(start, count);
    }
}

Github

Adding XML support and auxiliary classes

Yet another task was implementing settings via XML. As I am not very fond of creating Animators in XML (I find them obscure and hard to read), I decided to create a set of standard animations and make it possible to set them using flags. This approach also made setting animations via Java code easier. As creating CircularRevealAnimator would be different, I had to write two auxiliary classes: one for regular Properties and another for CircularReveal. A total of six classes were created:


PropertyCanny

class PropertyCanny {
    Animator propertyAnimator;
    public PropertyCanny(PropertyValuesHolder... holders) {
        this.propertyAnimator = ObjectAnimator.ofPropertyValuesHolder(holders);
    }
    public PropertyCanny(Property<?, Float> property, float start, float end) {
        this.propertyAnimator = ObjectAnimator.ofFloat(null, property, start, end);
    }
    public PropertyCanny(String propertyName, float start, float end) {
        this.propertyAnimator = ObjectAnimator.ofFloat(null, propertyName, start, end);
    }
    public Animator getPropertyAnimator(View child) {
        propertyAnimator.setTarget(child);
        return propertyAnimator.clone();
    }
}

PropertyIn

public class PropertyIn extends PropertyCanny implements InAnimator {
    public PropertyIn(PropertyValuesHolder... holders) {
        super(holders);
    }
    public PropertyIn(Property<?, Float> property, float start, float end) {
        super(property, start, end);
    }
    public PropertyIn(String propertyName, float start, float end) {
        super(propertyName, start, end);
    }
    public PropertyIn setDuration(long millis) {
        propertyAnimator.setDuration(millis);
        return this;
    }
    @Override
    public Animator getInAnimator(View inChild, View outChild) {
        return getPropertyAnimator(inChild);
    }
}

PropertyOut

public class PropertyOut extends PropertyCanny implements OutAnimator {
    public PropertyOut(PropertyValuesHolder... holders) {
        super(holders);
    }
    public PropertyOut(Property<?, Float> property, float start, float end) {
        super(property, start, end);
    }
    public PropertyOut(String propertyName, float start, float end) {
        super(propertyName, start, end);
    }
    public PropertyOut setDuration(long millis) {
        propertyAnimator.setDuration(millis);
        return this;
    }
    @Override
    public Animator getOutAnimator(View inChild, View outChild) {
        return getPropertyAnimator(outChild);
    }
}

RevealCanny

class RevealCanny {
    private final int gravity;
    public RevealCanny(int gravity) {
        this.gravity = gravity;
    }
    @SuppressLint("RtlHardcoded")
    protected int getCenterX(View view) {
        final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
        if (horizontalGravity == Gravity.LEFT) {
            return 0;
        } else if (horizontalGravity == Gravity.RIGHT) {
            return view.getWidth();
        } else { // (Gravity.CENTER_HORIZONTAL)
            return view.getWidth() / 2;
        }
    }
    protected int getCenterY(View view) {
        final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
        if (verticalGravity == Gravity.TOP) {
            return 0;
        } else if (verticalGravity == Gravity.BOTTOM) {
            return view.getHeight();
        } else { // (Gravity.CENTER_VERTICAL)
            return view.getHeight() / 2;
        }
    }
    public int getGravity() {
        return gravity;
    }
}

RevealIn

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RevealIn extends RevealCanny implements InAnimator {
    public RevealIn(int gravity) {
        super(gravity);
    }
    @Override
    public Animator getInAnimator(View inChild, View outChild) {
        float inRadius = (float) Math.hypot(inChild.getWidth(), inChild.getHeight());
        return ViewAnimationUtils.createCircularReveal(inChild, getCenterX(inChild),
                getCenterY(inChild), 0, inRadius);
    }
}

RevealOut

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RevealOut extends RevealCanny implements OutAnimator {
    public RevealOut(int gravity) {
        super(gravity);
    }
    @Override
    public Animator getOutAnimator(View inChild, View outChild) {
        float outRadius = (float) Math.hypot(outChild.getWidth(), outChild.getHeight());
        return ViewAnimationUtils.createCircularReveal(outChild, getCenterX(outChild),
                getCenterY(outChild), outRadius, 0);
    }
}


These classes make initializing animation quick and elegant. Instead of:


animator.setInAnimator(new InAnimator() {
            @Override
            public Animator getInAnimator(View inChild, View outChild) {
                return ObjectAnimator.ofFloat(inChild, View.ALPHA, 0, 1);
            }
        });
        animator.setOutAnimator(new OutAnimator() {
            @Override
            public Animator getOutAnimator(View inChild, View outChild) {
                return ObjectAnimator.ofFloat(outChild, View.ALPHA, 1, 0);
            }
        });


You can just write:


animator.setInAnimator(new PropertyIn(View.ALPHA, 0, 1));
animator.setOutAnimator(new PropertyOut(View.ALPHA, 1, 0));


It turned out to be even prettier than with lambda expressions. Then, using these classes, I created two lists of standard animations: one for Property PropertyAnimators and the other for CircularReveal RevealAnimators. Then I would use flags to find the list position in XML and put it in. As CircularRevealAnimator only works with Android 5 and higher, I had to create four parameters instead of two:

  • in — sets the animation for fading in;;
  • out — sets the animation for fading out
  • pre_lollipop_in — sets the animation for fading in, does not have CircularReveal in the list;
  • pre_lollipop_out — sets the animation for fading out, does not have CircularReveal in the list.

Later, when handling XML parameters, I determine the system version. If it’s higher than 5.0, I take values from in and out; if it’s lower, from pre_lollipop_in and pre_lollipop_out. If the version is lower than 5.0, but pre_lollipop_in and pre_lollipop_out are not assigned, values are taken from in and out anyway

Despite numerous issues, I managed to finish my work on CannyViewAnimator. It’s a little weird that every time I want to implement something I fancy, I have to use Java Reflection and dig deep. It makes me think that either there’s something wrong with Android SDK or I fancy a bit too much. If you have any ideas or suggestions, you’re welcome in the comment section.

Here’s the project link again.

Android
Development
ViewAnimator