Полный рабочий образец сценария анимации из трех фрагментов Gmail?


TL; DR: я ищу полный рабочий образец из того, что я буду называть "Gmail три фрагмента анимации" сценарий. В частности, мы хотим начать с двух фрагментов, вот так:

после некоторого события пользовательского интерфейса (например, нажав на что-то в фрагменте B), мы хотим:

  • фрагмент A, чтобы соскользнуть с экрана влево
  • фрагмент B, чтобы скользить к левому краю экрана и сжиматься, чтобы занять пятна, оставленных фрагмент
  • фрагмент C, чтобы скользить в правой части экрана и занять место, освобожденное фрагментом B

и, при нажатии кнопки назад, мы хотим, чтобы этот набор операций был отменен.

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


@Reto Meier внес этот популярный ответ на тот же базовый вопрос, указывающий, что вы будете использовать setCustomAnimations() с FragmentTransaction. Для сценария с двумя фрагментами (например, вы видите только фрагмент A изначально и хотите заменить его новым фрагментом B с использованием анимированных эффектов) я полностью согласен. Однако:

  • поскольку вы можете указать только одну анимацию "in" и одну анимацию "out", я не вижу, как вы будете обрабатывать все различные анимации, необходимые для сценария с тремя фрагментами
  • The <objectAnimator> в своем примере кода используется жесткие позиции в пикселях, и это казалось бы непрактичным, учитывая различные размеры экрана, но setCustomAnimations() требуются анимационные ресурсы, исключающие возможность определения этих вещей в Java
  • я не понимаю, как аниматоры объектов для масштабирования связаны с такими вещами, как android:layout_weight на LinearLayout для распределения пространства в процентах
  • я в недоумении относительно того, как фрагмент C обрабатывается в самом начале (GONE? android:layout_weight на 0? предварительно анимированные на шкала 0? что-то еще?)

@Роман Нурик указывает, что вы можете анимировать любое свойство, в том числе те, которые вы определяете сами. Это может помочь решить проблему жестких позиций, за счет изобретения собственного подкласса менеджера макетов. Это помогает некоторым, но я все еще озадачен остальной частью решения Рето.


автор эта запись пастебина показывает какой-то дразнящий псевдокод, в основном говоря что все три фрагмента будут находиться в контейнере изначально, а фрагмент C скрыт в начале через a hide() операции проводки. Мы тогда show() C и hide() A при возникновении события пользовательского интерфейса. Однако я не вижу, как это справляется с тем, что B меняет размер. Он также полагается на то, что вы, по-видимому, можете добавить несколько фрагментов в один и тот же контейнер, и я не уверен, является ли это надежным поведением в долгосрочной перспективе (не говоря уже о том, что он должен сломаться findFragmentById(), хотя я может жить с этим).


автор этот блог указывает, что Gmail не использует setCustomAnimations() вообще, но вместо этого напрямую использует аниматоры объектов ("вы просто меняете левое поле корневого представления + изменяете ширину правого представления"). Тем не менее, это все еще двухчастичное решение AFAICT, и реализация снова показала жесткие размеры проводов в пикселях.


я буду продолжать подключаться к этому, так что я могу закончить ответ это я сам когда-нибудь, но я действительно надеюсь, что кто-то разработал решение из трех фрагментов для этого сценария анимации и может опубликовать код (или ссылку на него). Анимации в Android заставляют меня хотеть вытащить мои волосы, и те из вас, кто видел меня, знают, что это в значительной степени бесплодное усилие.

6 121

6 ответов:

загрузил мое предложение в github (Работает со всеми версиями android, хотя аппаратное ускорение просмотра настоятельно рекомендуется для такого рода анимации. Для не аппаратных ускоренных устройств реализация кэширования растрового изображения должна соответствовать лучше)

демо-видео с анимацией составляет здесь (медленная частота кадров причина приведения экрана. Фактическая производительность очень быстро)


использование:

layout = new ThreeLayout(this, 3);
layout.setAnimationDuration(1000);
setContentView(layout);
layout.getLeftView();   //<---inflate FragmentA here
layout.getMiddleView(); //<---inflate FragmentB here
layout.getRightView();  //<---inflate FragmentC here

//Left Animation set
layout.startLeftAnimation();

//Right Animation set
layout.startRightAnimation();

//You can even set interpolators

объяснение:

создал новый пользовательский RelativeLayout (ThreeLayout) и 2 пользовательских анимации (MyScalAnimation, MyTranslateAnimation)

ThreeLayout получает вес левой панели как param, предполагая, что другой видимый вид имеет weight=1.

так new ThreeLayout(context,3) создает новое представление с 3 дочерними элементами и левой панелью с 1/3 общий экран. Другой вид занимает все доступное пространство.

он вычисляет ширину во время выполнения, более безопасная реализация заключается в том, что размеры вычисляются впервые в draw(). вместо in post ()

масштабирование и перевод анимации на самом деле изменить размер и переместить вид,а не псевдо-[масштаб, перемещение]. Обратите внимание, что fillAfter(true) нигде не используется.

Представление2 является right_of Представление1

и

View3-это right_of Изображение2

установив эти правила, RelativeLayout позаботится обо всем остальном. Анимация изменить margins (на ходу) и [width,height] по шкале

для доступа к каждому ребенку (так что вы можете раздуть его с вашим фрагментом вы можете позвонить

public FrameLayout getLeftLayout() {}

public FrameLayout getMiddleLayout() {}

public FrameLayout getRightLayout() {}

ниже показаны 2 анимации


Stage1

---в экране----------!----- Вон - - - -

[View1] [_____View2_____][_____View3_____]

Stage2

-- OUT -!-------- В экран - - - - - -

[View1][View2] [_____View3____]

хорошо, вот мое собственное решение, полученное из приложения электронной почты AOSP, за предложение @Christopher в комментариях к вопросу.

https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePane

решение@weakwire напоминает мое, хотя он использует классический Animation, а не аниматоры, а он использует RelativeLayout правила для обеспечения позиционирования. С точки зрения щедрости, он, вероятно, получит щедрость, если кто-то другой с решение для дождевика все же публикует ответ.


в двух словах ThreePaneLayout в этом проекте-это LinearLayout подкласс, предназначенный для работы в пейзаж с тремя детьми. Эти детские ширины могут быть установлены в XML-формате макета, с помощью любых желаемых средств - я показываю с помощью весов, но у вас может быть определенная ширина, установленная ресурсами измерения или чем-то еще. Третий ребенок-фрагмент C в вопросе - должен иметь ширину нуля.

package com.commonsware.android.anim.threepane;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;

public class ThreePaneLayout extends LinearLayout {
  private static final int ANIM_DURATION=500;
  private View left=null;
  private View middle=null;
  private View right=null;
  private int leftWidth=-1;
  private int middleWidthNormal=-1;

  public ThreePaneLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    initSelf();
  }

  void initSelf() {
    setOrientation(HORIZONTAL);
  }

  @Override
  public void onFinishInflate() {
    super.onFinishInflate();

    left=getChildAt(0);
    middle=getChildAt(1);
    right=getChildAt(2);
  }

  public View getLeftView() {
    return(left);
  }

  public View getMiddleView() {
    return(middle);
  }

  public View getRightView() {
    return(right);
  }

  public void hideLeft() {
    if (leftWidth == -1) {
      leftWidth=left.getWidth();
      middleWidthNormal=middle.getWidth();
      resetWidget(left, leftWidth);
      resetWidget(middle, middleWidthNormal);
      resetWidget(right, middleWidthNormal);
      requestLayout();
    }

    translateWidgets(-1 * leftWidth, left, middle, right);

    ObjectAnimator.ofInt(this, "middleWidth", middleWidthNormal,
                         leftWidth).setDuration(ANIM_DURATION).start();
  }

  public void showLeft() {
    translateWidgets(leftWidth, left, middle, right);

    ObjectAnimator.ofInt(this, "middleWidth", leftWidth,
                         middleWidthNormal).setDuration(ANIM_DURATION)
                  .start();
  }

  public void setMiddleWidth(int value) {
    middle.getLayoutParams().width=value;
    requestLayout();
  }

  private void translateWidgets(int deltaX, View... views) {
    for (final View v : views) {
      v.setLayerType(View.LAYER_TYPE_HARDWARE, null);

      v.animate().translationXBy(deltaX).setDuration(ANIM_DURATION)
       .setListener(new AnimatorListenerAdapter() {
         @Override
         public void onAnimationEnd(Animator animation) {
           v.setLayerType(View.LAYER_TYPE_NONE, null);
         }
       });
    }
  }

  private void resetWidget(View v, int width) {
    LinearLayout.LayoutParams p=
        (LinearLayout.LayoutParams)v.getLayoutParams();

    p.width=width;
    p.weight=0;
  }
}

однако, во время выполнения, независимо от того, как вы изначально настроили ширину, управление шириной берет на себя ThreePaneLayout при первом использовании hideLeft() чтобы перейти от показа того, что вопрос называется фрагментами A и B к фрагментам B и C. В терминологии ThreePaneLayout -- который не имеет никакой конкретной связи с фрагментами -- эти три части left,middle и right. В то время, когда вы звоните hideLeft(), мы записываем размеры left и middle и обнуления всех Весов, которые были использованы на любой из трех, так что мы можем полностью контролируйте размеры. В момент времени hideLeft() мы установить размер right чтобы быть оригинальным размером middle.

анимация в два раза:

  • использовать ViewPropertyAnimator чтобы выполнить перевод трех виджетов влево по ширине left, используя аппаратный слой
  • использовать ObjectAnimator на пользовательском псевдо-собственность middleWidth изменить middle ширина от того, что он начал с оригинальной ширина left

(возможно, что лучше использовать AnimatorSet и ObjectAnimators для всех этих, хотя это работает на данный момент)

(также возможно, что middleWidthObjectAnimator отрицает значение аппаратного уровня, так как это требует довольно непрерывной недействительности)

(это наверняка возможно, что у меня все еще есть пробелы в моем понимании анимации, и что мне нравится в скобках Ведомости)

чистый эффект заключается в том, что left соскальзывает с экрана,middle слайды в исходное положение и размер left и right переводится сразу за middle.

showLeft() просто переворачивает процесс, с той же смесью аниматоров, только с обратными направлениями.

деятельность использует ThreePaneLayout провести пару ListFragment виджеты и Button. Выбор чего-то в левом фрагменте добавляет (или обновляет содержимое в) средний фрагмент. Выбор чего-то в среднем фрагменте устанавливает заголовок Button плюс выполняет hideLeft() на ThreePaneLayout. Нажатие назад, если мы спрятали левую сторону, выполнит showLeft(); в противном случае BACK завершает работу. Так как это не использует FragmentTransactions для влияния на анимацию, мы застряли, управляя этим "задним стеком" сами.

проект, связанный с вышеизложенным, использует собственные фрагменты и собственную структуру аниматора. У меня есть другая версия того же самого проект, который использует фрагменты поддержки Android backport и NineOldAndroids для анимации:

https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePaneBC

backport отлично работает на 1-м поколении Kindle Fire, хотя анимация немного отрывистая, учитывая более низкие аппаратные характеристики и отсутствие поддержки аппаратного ускорения. Оба варианта кажутся гладкими на Nexus 7 и другие текущие поколения таблетки.

я, безусловно, открыт для идей о том, как улучшить это решение или другие решения, которые предлагают явные преимущества по сравнению с тем, что я сделал здесь (или что использовал @weakwire).

еще раз спасибо всем, кто участвовал!

мы создали библиотеку под названием PanesLibrary, которая решает эту проблему. Это еще более гибкий, чем то, что было предложено ранее, потому что:

  • каждая панель может быть динамически размера
  • Он позволяет использовать любое количество панелей (а не только 2 или 3)
  • фрагменты внутри панелей правильно сохраняются при изменении ориентации.

вы можете проверить это здесь: https://github.com/Mapsaurus/Android-PanesLibrary

вот демо:http://www.youtube.com/watch?v=UA-lAGVXoLU&feature=youtu.be

Это в основном позволяет легко добавлять любое количество динамических размеров панелей и прикреплять фрагменты к этим панелям. Надеюсь, вы найдете его полезным! :)

построение одного из примеров, которые вы связали с (http://android.amberfog.com/?p=758), Как насчет анимации layout_weight собственность? Таким образом, вы можете анимировать изменение веса 3 фрагментов вместе, и вы получите бонус, что все они хорошо скользят вместе:

начните с простого макета. Так как мы будем анимировать layout_weight нам нужен LinearLayout в качестве корневого вид для 3-х панелей.:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
    android:id="@+id/panel1"
    android:layout_width="0dip"
    android:layout_weight="1"
    android:layout_height="match_parent"/>

<LinearLayout
    android:id="@+id/panel2"
    android:layout_width="0dip"
    android:layout_weight="2"
    android:layout_height="match_parent"/>

<LinearLayout
    android:id="@+id/panel3"
    android:layout_width="0dip"
    android:layout_weight="0"
    android:layout_height="match_parent"/>
</LinearLayout>

затем демо класс:

public class DemoActivity extends Activity implements View.OnClickListener {
    public static final int ANIM_DURATION = 500;
    private static final Interpolator interpolator = new DecelerateInterpolator();

    boolean isCollapsed = false;

    private Fragment frag1, frag2, frag3;
    private ViewGroup panel1, panel2, panel3;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        panel1 = (ViewGroup) findViewById(R.id.panel1);
        panel2 = (ViewGroup) findViewById(R.id.panel2);
        panel3 = (ViewGroup) findViewById(R.id.panel3);

        frag1 = new ColorFrag(Color.BLUE);
        frag2 = new InfoFrag();
        frag3 = new ColorFrag(Color.RED);

        final FragmentManager fm = getFragmentManager();
        final FragmentTransaction trans = fm.beginTransaction();

        trans.replace(R.id.panel1, frag1);
        trans.replace(R.id.panel2, frag2);
        trans.replace(R.id.panel3, frag3);

        trans.commit();
    }

    @Override
    public void onClick(View view) {
        toggleCollapseState();
    }

    private void toggleCollapseState() {
        //Most of the magic here can be attributed to: http://android.amberfog.com/?p=758

        if (isCollapsed) {
            PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3];
            arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 0.0f, 1.0f);
            arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 1.0f, 2.0f);
            arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 2.0f, 0.0f);
            ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION);
            localObjectAnimator.setInterpolator(interpolator);
            localObjectAnimator.start();
        } else {
            PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3];
            arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 1.0f, 0.0f);
            arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 2.0f, 1.0f);
            arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 0.0f, 2.0f);
            ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION);
            localObjectAnimator.setInterpolator(interpolator);
            localObjectAnimator.start();
        }
        isCollapsed = !isCollapsed;
    }

    @Override
    public void onBackPressed() {
        //TODO: Very basic stack handling. Would probably want to do something relating to fragments here..
        if(isCollapsed) {
            toggleCollapseState();
        } else {
            super.onBackPressed();
        }
    }

    /*
     * Our magic getters/setters below!
     */

    public float getPanel1Weight() {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)     panel1.getLayoutParams();
        return params.weight;
    }

    public void setPanel1Weight(float newWeight) {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel1.getLayoutParams();
        params.weight = newWeight;
        panel1.setLayoutParams(params);
    }

    public float getPanel2Weight() {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams();
        return params.weight;
    }

    public void setPanel2Weight(float newWeight) {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams();
        params.weight = newWeight;
        panel2.setLayoutParams(params);
    }

    public float getPanel3Weight() {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams();
        return params.weight;
    }

    public void setPanel3Weight(float newWeight) {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams();
        params.weight = newWeight;
        panel3.setLayoutParams(params);
    }


    /**
     * Crappy fragment which displays a toggle button
     */
    public static class InfoFrag extends Fragment {
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle     savedInstanceState) {
            LinearLayout layout = new LinearLayout(getActivity());
            layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            layout.setBackgroundColor(Color.DKGRAY);

            Button b = new Button(getActivity());
            b.setOnClickListener((DemoActivity) getActivity());
            b.setText("Toggle Me!");

            layout.addView(b);

            return layout;
        }
    }

    /**
     * Crappy fragment which just fills the screen with a color
     */
    public static class ColorFrag extends Fragment {
        private int mColor;

        public ColorFrag() {
            mColor = Color.BLUE; //Default
        }

        public ColorFrag(int color) {
            mColor = color;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            FrameLayout layout = new FrameLayout(getActivity());
            layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

            layout.setBackgroundColor(mColor);
            return layout;
        }
    }
}

также этот пример не использует FragmentTransactions для достижения анимации (скорее, он анимирует представления, к которым прикреплены фрагменты), поэтому вам нужно будет сделать все транзакции backstack/fragment самостоятельно, но по сравнению с усилиями по получению анимации, работающей хорошо, это не похоже на плохой компромисс:)

ужасное видео с низким разрешением в действии:http://youtu.be/Zm517j3bFCo

это не использование фрагментов.... Это пользовательский макет с 3 детьми. Когда вы нажимаете на сообщение, вы смещаете 3 ребенка с помощью offsetLeftAndRight() и аниматор.

на JellyBean вы можете включить "показать границы макета" в настройках "Параметры разработчика". Когда анимация слайдов будет завершена, вы все равно увидите, что левое меню все еще там, но под средней панелью.

это похоже на Кирилл Мотье Fly-in меню приложения, но с 3 элементами вместо 2.

Additionnally, в ViewPager из третьих детей является еще одним признаком такого поведения:ViewPager обычно использует фрагменты (я знаю, что они не должны, но я никогда не видел реализации другой, что Fragment), и так как вы не можете использовать Fragments внутри Fragment 3 ребенка, вероятно, не фрагменты....

в настоящее время я пытаюсь сделать что-то подобное, за исключением того, что масштаб фрагмента B занимает доступное пространство и что панель 3 может быть открыта одновременно, если есть достаточно места. Вот мое решение до сих пор, но я не уверен, что буду придерживаться его. Я надеюсь, что кто-то даст ответ, показывающий правильный путь.

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

Итак, я анимирую макет, я не использую фрагменты транзакции. Я обрабатываю кнопку "назад" вручную, чтобы закрыть фрагмент C, если он открыт.

FragmentB используйте layout_toLeftOf/ToRightOf, чтобы выровнять его по фрагменту A и C.

когда мое приложение запускает событие для отображения фрагмента C,я одновременно выдвигаю фрагмент C и выдвигаю фрагмент A. (2 отдельные анимации). И наоборот, когда фрагмент A открыт, я одновременно закрываю C.

в портретном режиме или на меньшем экране я использую немного другой макет и слайд-фрагмент C по экрану.

чтобы использовать процент для ширины фрагмента A и C, я думаю, вам придется вычислить его во время выполнения... (?)

вот план действия:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rootpane"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <!-- FRAGMENT A -->
    <fragment
        android:id="@+id/fragment_A"
        android:layout_width="300dp"
        android:layout_height="fill_parent"
        class="com.xyz.fragA" />

    <!-- FRAGMENT C -->
    <fragment
        android:id="@+id/fragment_C"
        android:layout_width="600dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        class="com.xyz.fragC"/>

    <!-- FRAGMENT B -->
    <fragment
        android:id="@+id/fragment_B"
        android:layout_width="match_parent"
        android:layout_height="fill_parent"
        android:layout_marginLeft="0dip"
        android:layout_marginRight="0dip"
        android:layout_toLeftOf="@id/fragment_C"
        android:layout_toRightOf="@id/fragment_A"
        class="com.xyz.fragB" />

</RelativeLayout>

анимация для слайда FragmentC В или из:

private ValueAnimator createFragmentCAnimation(final View fragmentCRootView, boolean slideIn) {

    ValueAnimator anim = null;

    final RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) fragmentCRootView.getLayoutParams();
    if (slideIn) {
        // set the rightMargin so the view is just outside the right edge of the screen.
        lp.rightMargin = -(lp.width);
        // the view's visibility was GONE, make it VISIBLE
        fragmentCRootView.setVisibility(View.VISIBLE);
        anim = ValueAnimator.ofInt(lp.rightMargin, 0);
    } else
        // slide out: animate rightMargin until the view is outside the screen
        anim = ValueAnimator.ofInt(0, -(lp.width));

    anim.setInterpolator(new DecelerateInterpolator(5));
    anim.setDuration(300);
    anim.addUpdateListener(new AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            Integer rightMargin = (Integer) animation.getAnimatedValue();
            lp.rightMargin = rightMargin;
            fragmentCRootView.requestLayout();
        }
    });

    if (!slideIn) {
        // if the view was sliding out, set visibility to GONE once the animation is done
        anim.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                fragmentCRootView.setVisibility(View.GONE);
            }
        });
    }
    return anim;
}