Статья была впервые опубликована здесь.
Всем привет! Мне очень нравится работать с анимациями — в каждом
О чём вообще речь
Но сначала представим для наглядности ситуацию, банальную в 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— выставляет анимацию на появление, не содержит в спискеCircularRevealpre_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. Вообще, странно то, что каждый раз, как я хочу реализовать
