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