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
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
.
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
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 especiallyCircularRevealAnimator
- 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 forchild
visibility switchingTransitionViewAnimator
would be inherited fromViewAnimator
and used for working withTransition
- 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 theAnimator
of the appearing (fading in)child
OutAnimator
— responsible for theAnimator
of the disappearing (fading out)child
CannyTransition
— responsible forTransition
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 thechild
with the specified index void setDisplayedChildId(@IdRes int id)
— displays thechild
with the specified idvoid setDisplayedChild(View view)
— displays a specificchild
int getDisplayedChildIndex()
— gets the index of the displayedchild
View getDisplayedChild()
— gets the displayedchild
int getDisplayedChildId()
— gets the id of the displayedchild
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(); } } }
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; } }
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 roadblock here: it was impossible to launch Animator
for a 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 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 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
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); } }
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 inout
— sets the animation for fading outpre_lollipop_in
— sets the animation for fading in, does not haveCircularReveal
in the listpre_lollipop_out
— sets the animation for fading out, does not haveCircularReveal
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.