Статья была впервые опубликована здесь.
Всем привет! Мне очень нравится работать с анимациями — в каждом
О чём вообще речь
Но сначала представим для наглядности ситуацию, банальную в ViewAnimator
. Единственное, но важное отличие BetterViewAnimator
от его предка — это умение работать с id ресурсов. Но он не идеален.
Что такое ViewAnimator?
ViewAnimator — это View
, который наследуется от FrameLayout
и у которого в конкретный момент времени виден только один из его child
. Для переключения видимого child
есть набор методов.
Важным минусом BetterViewAnimator
является умение работать только с устаревшим AnimationFramework
. И в этой ситуации приходит на помощь CannyViewAnimator
. Он поддерживает работу с Animator
и AppCompat Transition
.
С чего всё началось
Во время разработки очередного экрана BetterViewAnimator
, но ViewAnimator
может работать только с Animation. Поиски альтернативы на Github, к сожалению, не увенчались успехом — достойных не было, а был только Android View Controller, но он абсолютно не гибок и поддерживает только восемь заранее заданных в нём анимаций. Это означало только одно: придётся писать всё самому.
Что же я хочу получить
Первое, что я решил сделать — это продумать то, что я в итоге хочу получить:
- возможность всё так же управлять видимостью
child
; - возможность использовать
Animator
и в особенностиCircularRevealAnimator
; - возможность запускать анимации как последовательно, так и параллельно (
ViewAnimator
умеет только последовательно); - возможность использовать
Transition
; - сделать набор стандартных анимаций с возможностью их выставления через xml;
- гибкость работы, возможность выставлять для отдельного
child
свою анимацию.
Определившись с желаниями, я начал продумывать «архитектуру» будущего проекта. Получилось три части:
ViewAnimator
— отвечает на переключение видимостиchild
;TransitionViewAnimator
— наследуется отViewAnimator
и отвечает за работу с Transition;CannyViewAnimator
— наследуется отTransitionViewAnimator
и отвечает за работу сAnimator
.
Выставление Animator
и Transition
я решил сделать с помощью интерфейса с двумя параметрами: child
, который будет появляться, и child
, который будет исчезать. Каждый раз, когда сменяется видимый child
, из реализации интерфейса будет забираться необходимая анимация. Интерфейса будет три:
InAnimator
— отвечающий заAnimator
появляющегосяchild
;OutAnimator
— отвечающий заAnimator
исчезающегоchild
;CannyTransition
— отвечающий заTransition
.
Интерфейс для Transition я решил сделать один, так как Transition накладывается сразу на все появляющиеся и исчезающие child
. Концепция была продумана, и я приступил к разработке.
ViewAnimator
Со своим базовым классом я не стал особо мудрить и решил сделать копирку с ViewAnimator из SDK. Я лишь выбросил из него работу с Animation
и оптимизировал методы в нём, так как многие из них мне показались избыточными. Также я не забыл добавить и методы из BetterViewAnimator
. Итоговый список важных для работы с ним методов получился таким:
void setDisplayedChildIndex(int inChildIndex)
— отображаетchild
с заданным индексом;void setDisplayedChildId(@IdRes int id)
— отображаетchild
с заданным id;void setDisplayedChild(View view)
— отображает конкретныйchild
;int getDisplayedChildIndex()
— получение индекса отображаемогоchild
;View getDisplayedChild()
— получение отображаемогоchild
;int getDisplayedChildId()
— получение id отображаемогоchild
.
Немного подумав, я решил дополнительно сохранять позицию текущего видимого child
в onSaveInstanceState()
и восстанавливать еёonRestoreInstanceState(Parcelable state)
, тут же отображая его.
Итоговый код получился таким:
ViewAnimator
<code class="post-page__inline-code">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(); } } }</code>
TransitionViewAnimator
Закончив с ViewAnimator
, я приступил к довольно простой, но от этого не менее интересной задаче: сделать поддержку Transition. Суть работы такова: при вызове переопределённого метода changeVisibility (View inChild, View outChild)
подготавливается анимация. Из заданного CannyTransition
с помощью интерфейса забирается Transition
и записывается в поле класса.
CannyTransition
<code class="post-page__inline-code">public interface CannyTransition { Transition getTransition(View inChild, View outChild); }</code>
Затем в отдельном методе выполняется запуск этого Transition
. Я решил сделать запуск отдельным методом с заделом на будущее — дело в том, что запуск Transition осуществляется с помощью метода TransitionManager.beginDelayedTransition
, а это накладывает некоторые ограничения. Ведь Transition выполнится только для тех View, которые поменяли свои свойства за некоторый промежуток времени после вызова TransitionManager.beginDelayedTransition
. Так как в дальнейшем планируется внедрение Animator’ов, которые могут длится относительно долгое время, то TransitionManager.beginDelayedTransition
нужно вызывать непосредственно перед сменой Visibility. Ну, и далее я вызываю super.changeVisibility(inChild, outChild);
, который меняет visibility
у нужных child
.
TransitionViewAnimator
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
Вот я и добрался до основной прослойки. Изначально я хотел воспользоваться LayoutTransition для управления Animator’ами, но мои мечты разбились о невозможность без костылей выполнить с его помощью анимации параллельно. Также дополнительные проблемы создавали остальные минусы LayoutTransition
вроде необходимости выставлять длительность для AnimatorSet
, невозможности ручного прерывания и пр. Было принято решение написать свою логику работы. Все выглядело очень даже просто: запускаем Animator для исчезающего child
, на его окончание выставляем ему Visibility. GONE
и тут же делаем появляющийся child
видимым и запускаем Animator для него.
Тут я наткнулся на первую проблему: нельзя запустить Animator для неприаттаченной View (это та, у которой ещё не был выполнен onAttach
или уже сработал onDetach
). Это не давало мне менять видимость child
в конструкторе или любом другом методе, который срабатывает раньше onAttach
. Предвидя кучу разнообразных ситуаций, где это может понадобится, и не менее маленькую кучу issues на Github, я решил попытаться исправить положение. К сожалению, самое простое решение в виде вызова метода isAttachedToWindow()
упиралось в невозможность его вызова до 19 версии API, а мне очень хотелось иметь поддержку с 14 API.
Однако у View существует OnAttachStateChangeListener
, и я не преминул им воспользоваться. Я переопределил метод void addView(View child, int index, ViewGroup. LayoutParams params)
и на каждую добавленную View вешал этот Listener. Далее я помещал в HashMap ссылку на саму View и булеву переменную, обозначающую его состояние. Если срабатывал onViewAttachedToWindow(View v)
, я ставил значение true
, а если onViewDetachedFromWindow(View v)
, то false
. Теперь, перед самым запуском Animator’а, я мог проверять состояние View
и мог решить, стоит ли вообще запускать Animator
.
После преодоления первой «баррикады» я сделал два интерфейса для получения Animator’ов: InAnimator
и OutAnimator
.
InAnimator
public interface InAnimator { Animator getInAnimator(View inChild, View outChild); }
OutAnimator:
public interface OutAnimator { Animator getOutAnimator(View inChild, View outChild); }
Всё шло гладко, пока передо мной не встала новая проблема: после выполнения Animator’а нужно восстановить состояние View
.
Ответа на StackOverflow я так и не нашёл. После получаса мозгового штурма я решил воспользоваться методом reverse у ValueAnimator
, сделав его длительность равной нулю.
if (animator instanceof ValueAnimator) { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animation.removeListener(this); animation.setDuration(0); ((ValueAnimator) animation).reverse(); } }); }
Это помогло, и я даже дал тот самый ответ на StackOverflow.
Сразу же после решения этой проблемы возникла другая: CircularRevealAnimator
не выполняет свою анимацию, если у View ещё не был выполнен onMeasure
.
Это было плохой новостью, так как у ViewAnimator
невидимые child
имеют Visibility. GONE
. Это значит, что они не измеряются вплоть до того момента, пока им не выставят другой тип Visibility
— VISIBLE
или INVISIBLE
. Даже если бы перед началом анимации я изменил Visibility
на INVISIBLE
, то это не решило бы проблемы. Так как измерение размеров View происходит при отрисовке кадра, а отрисовка кадров происходит асинхронно, то нет никакой гарантии, что к моменту старта Animator’а View была бы измерена. Выставлять задержку или использовать onPreDrawListener
мне крайне не хотелось, поэтому по умолчанию я решил использовать Visibility. INVISIBLE
вместо Visibility. GONE
.
В голове прокручивались сцены ужасов по мотивам того, как мои View измеряются при инфлейте (хотя им это совсем не надо), что сопровождается визуальными лагами. Поэтому я решил провести небольшой тест, измеряя время инфлейта, с Visibility. INVISIBLE
и Visibility. GONE
c 10 View и вложенностью 5. Тесты показали, что разница не превышала 1 миллисекунды. То ли я не заметил, как телефоны стали гораздо мощнее, то ли Android так хорошо оптимизировали, но мне смутно вспоминается, что Visibility. INVISIBLE
плохо влиял на производительность. Ну да ладно, проблема была побеждена.
Не успев опомниться от предыдущей «схватки», я бросился в следующую. Так как во FrameLayout
child
лежат друг над другом, то при одновременном выполнении InAnimator
и OutAnimator
возникает ситуация, когда в зависимости от индекса child
анимация выглядит ViewGroup
, игрался со свойством Z
и пробовал ещё кучу всякого.
Наконец, пришла идея в начале анимации просто удалить нужную View из контейнера, добавить её наверх, а в конце анимации опять удалить и затем вернуть на исходное место. Идея сработала, но на слабых устройствах анимации подлагивали. Подвисание происходило requestLayout()
, который пересчитывает и перерисовывает их. Пришлось лезть в дебри класса ViewGroup
. Спустя несколько минут изучения я пришел к выводу, что порядок расположения View внутри ViewGroup
зависит всего лишь от одного массива, а дальше наследники ViewGroup (к примеру, FrameLayout
или LinearLayout
) уже решают, как его отобразить. Увы, массив, а также методы работы с ним, были помечены private
. Но была и хорошая новость: в Java это не проблема, так как есть Java Reflection. С помощью Java Reflection я воспользовался методами работы с массивом и теперь мог управлять положением нужной мне View напрямую. Получился вот такой метод:
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(); } }
Этот метод выставляет нужную мне позицию для View. Перерисовку в конце этих манипуляций вызывать не нужно — за вас это сделает анимация. Теперь перед началом анимации я мог положить нужную мне View наверх, а в конце анимации вернуть обратно. Итак, основная часть рассказа о CannyViewAnimator закончена.
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); } }
Добавляем поддержку XML и классы-помощники
Новая задача: добавить возможность настройки с помощью XML. Так как я очень сильно не люблю создание Animator в XML (они мне кажутся CircularRevealAnimator
отличается от стандартного, пришлось написать два типа CircularReveal
.
В итоге получилось шесть классов:
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); } }
С их помощью инициализация анимаций стало проще и изящнее. Вместо:
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); } });
Можно просто написать:
animator.setInAnimator(new PropertyIn(View.ALPHA, 0, 1)); animator.setOutAnimator(new PropertyOut(View.ALPHA, 1, 0));
Получилось даже посимпатичнее, чем с использованием Property
— PropertyAnimators, другой для CircularReaval
— RevealAnimators. Далее я с помощью флагов находил в XML позицию в этих списках и подставлял его. Так как CircularRevealAnimator
работает только с Android 5 и выше. Пришлось создать четыре параметра вместо двух:
in
— выставляет анимацию на появлениеout
— выставляет анимацию на исчезновениеpre_lollipop_in
— выставляет анимацию на появление, не содержит в спискеCircularReveal
pre_lollipop_out
— выставляет анимацию на исчезновение, не содержит в спискеCircularReveal
Далее при разборе параметров из XML я определяю версию системы. Если она выше, чем 5.0, то беру значения из in
и out
; если ниже, то из pre_lollipop_in
и pre_lollipop_out
. Если версия ниже чем 5.0, но pre_lollipop_in и pre_lollipop_out
не заданы, то значения берутся из in и out.
Несмотря на множество проблем, я всё же успешно завершил CannyViewAnimator. Вообще, странно то, что каждый раз, как я хочу реализовать