Полный рабочий образец сценария анимации из трех фрагментов 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 ответов:
загрузил мое предложение в 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внутриFragment3 ребенка, вероятно, не фрагменты....
в настоящее время я пытаюсь сделать что-то подобное, за исключением того, что масштаб фрагмента 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; }