Статьи

Как мы разработали анимацию гильотинного меню для Android

Вы, наверное, читали нашу  историю  о том, как наш дизайнер Виталий Рубцов и разработчик iOS Максим Лазебный создали нетрадиционную анимацию верхней панели, получившую зловещее название — Guillotine Menu  (анимацию iOS можно увидеть на  Dribbble  и  GitHub ). Вскоре после этого наш Android-разработчик Дмитрий Денисенко взял на себя задачу реализовать ту же анимацию, но на платформе Android (посмотрите на  GitHub ). Он даже не мог предсказать, с какими трудностями ему придется столкнуться и насколько глубоко ему придется погрузиться в поисках решения.

Когда начать?

Сначала я хотел прибегнуть к стандартным решениям для реализации компонента для Android. Ведь это казалось возможным с первого взгляда. У меня был план использовать  ObjectAnimation  для реализации поворота вида навигации. Я также хотел добавить BounceInterpolator  по умолчанию  для достижения эффекта отскока, когда меню сталкивается с левой границей экрана. Тем не менее,  BounceInterpolator,  казалось, сделал отскок слишком мощным, как если бы это был отскок футбольного мяча, а не металлической гильотины.

По умолчанию  BounceInterpolator  не предоставляет никаких параметров для настройки, поэтому у меня не было другого выбора, кроме как написать свой собственный интерполятор. Предполагалось, что он не только добавит отскок, но и создаст эффект ускорения свободного падения, чтобы анимация выглядела более естественной.

Компонент Гильотина в нашей анимации состоит из вращения Гильотины, отскока Гильотины и отскока панели действий. Более того, я использовал два пользовательских интерполятора для реализации эффектов ускорения свободного падения и отскока. Теперь пришло время провести вас через процесс разработки.

Как мы реализовали ротацию гильотинного меню

Мне нужно было сделать две вещи, чтобы вращать анимацию: найти центр вращения и реализовать  ObjectAnimation,  чтобы сделать фактическое вращение.

Прежде чем я смог вычислить центр вращения, я должен был поместить макет на экран.

private void setUpOpeningView(final View openingView) {

   if (mActionBarView != null) {

       mActionBarView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

           @Override

           public void onGlobalLayout() {

               if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {

                   mActionBarView.getViewTreeObserver().removeOnGlobalLayoutListener(this);

               } else {

                   mActionBarView.getViewTreeObserver().removeGlobalOnLayoutListener(this);

               }

               mActionBarView.setPivotX(calculatePivotX(openingView));

               mActionBarView.setPivotY(calculatePivotY(openingView));

           }

       });

   }

}



private float calculatePivotY(View burger) {

   return burger.getTop() + burger.getHeight() / 2

}



private float calculatePivotY(View burger) {

   return burger.getTop() + burger.getHeight() / 2;

}

После этого мне осталось добавить всего несколько строк кода:

ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(mGuillotineView, "rotation", GUILLOTINE_OPENED_ANGLE, GUILLOTINE_CLOSED_ANGLE);

/* setting duration, listeners, interpolator, etc. */

rotationAnimator.start();

Центр вращения на самом деле является центром гамбургера. Для анимации требуются два гамбургера: один на главной панели действий, а другой — на гильотине. Чтобы анимация выглядела гладко, оба гамбургера должны были быть одинаковыми и использовать одинаковые координаты. Чтобы добиться этого, я просто создал гамбургер на панели инструментов (который вы не видите) и выровнял его по центру гамбургера меню Гильотина. 

Как мы реализовали Free Fall и Rebound

Для реализации анимации меню «Гильотина» для iOS мой коллега  Максим Лазебный использовал  стандартный   класс UIDynamicItemBehavior, который был настроен с помощью свойств эластичности и сопротивления. Тем не менее, это не так просто на Android.

[Стандартный Android-интерполятор]

Как я упоминал ранее, я мог бы использовать BounceInterpolator  по умолчанию  для поворота макета, но, похоже, он дал слишком мягкий  отскок (как будто наша гильотина была шаром). Вот почему я попытался реализовать собственный интерполятор. Это должно было добавить ускорение анимации.

Скорость интерполяции варьируется от 0 до 1. В моем случае угол поворота идет от 0 ° до 90 ° (по часовой стрелке). Это означает, что под углом 0 ° частота интерполяции также будет равна «0» (начальная позиция), а когда угол равен 90 °, коэффициент интерполяции будет равен «1» (конечная позиция).

Поскольку наша интерполяция имеет  квадратичную зависимость, она допускает как отскок, так и свободное падение, как на  анимированном скриншоте Виталия . Я должен был вспомнить мой курс математики в средней школе, чтобы построить специальный интерполятор. После короткого мозгового штурма я взял тетрадь и нарисовал график функций, который иллюстрирует зависимость свойств объекта от времени.

[Пользовательский интерполятор]

Я написал три квадратных уравнения, которые соответствуют графику.

public class GuillotineInterpolator implements TimeInterpolator {

   public static final float ROTATION_TIME = 0.46667f;
   public static final float FIRST_BOUNCE_TIME = 0.26666f;
   public static final float SECOND_BOUNCE_TIME = 0.26667f;


   public GuillotineInterpolator() {
   }

   public float getInterpolation(float t) {
       if (t < ROTATION_TIME) return rotation(t);
       else if (t < ROTATION_TIME + FIRST_BOUNCE_TIME) return firstBounce(t);
       else return secondBounce(t);
   }

   private float rotation(float t) {
       return 4.592f * t * t;
   }

   private float firstBounce(float t) {
       return 2.5f * t * t - 3f * t + 1.85556f;
   }

   private float secondBounce(float t) {
       return 0.625f * t * t - 1.08f * t + 1.458f;
   }

How We Implemented the Action Bar’s Rebound

Now our Guillotine menu could fall and make a rebound when it collides with the left border of the screen. However, there was one more rebound I still had to implement. When the Guillotine menu comes back to the initial state, it collides with the action bar producing a bouncing effect. For that, I needed another interpolator.

Here the graph initiates and terminates at 0° angle, but a quadratic dependence is built on the same principle as in the previous case.

public class ActionBarInterpolator implements TimeInterpolator {

   private static final float FIRST_BOUNCE_PART = 0.375f;
   private static final float SECOND_BOUNCE_PART = 0.625f;

   @Override
   public float getInterpolation(float t) {
       if (t < FIRST_BOUNCE_PART) {
           return (-28.4444f) * t * t + 10.66667f * t;
       } else if (t < SECOND_BOUNCE_PART) {
           return (21.33312f) * t * t - 21.33312f * t + 4.999950f;
       } else {
           return (-9.481481f) * t * t + 15.40741f * t - 5.925926f;
       }
   }
}

As a result, we’ve got three ObjectAnimation instances: the Guillotine’s opening and shutting, the action bar’s rotation, and two interpolators: the Guillotine’s fall and the action bar’s rebound. All I needed to do afterwards is set up the interpolations to the appropriate animations, start the action bar’s rebound right after shutting the menu, and bind the start of animations with the tap on an appropriate hamburger.

ObjectAnimator rotationAnimator = initAnimator(ObjectAnimator.ofFloat(mGuillotineView, ROTATION, GUILLOTINE_CLOSED_ANGLE, GUILLOTINE_OPENED_ANGLE));

rotationAnimator.setInterpolator(mInterpolator);

rotationAnimator.setDuration(mDuration);

rotationAnimator.addListener(new Animator.AnimatorListener() {...});

That’s it. Building the animation was quite a challenge, but it was totally worth it! Now our smooth Guillotine menu is available for both platforms, iOS and Android.

Read also: How we created FlipViewPager animation for Android

Features Planned

I intend to add a few new effects to the Guillotine menu animation. They include swipe transitions, right-to-left layout support, and a horizontal layout orientation. Stay tuned and watch for our updates.

You can find the sample of the project and its design here:

[Dmytro Denysenko, Android developer at Yalantis]