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

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

two fragments

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

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

И, нажав кнопку BACK, мы хотим, чтобы этот набор операций был отменен.

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


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

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

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


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


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


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

4b9b3361

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

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

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


Вкратце, 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. Нажатие BACK, если мы спрятали левую сторону, выполнит showLeft(); в противном случае BACK завершает действие. Так как это не использует FragmentTransactions для воздействия на анимацию, мы застряли в управлении этим "back stack" сами.

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

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

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

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

Еще раз спасибо всем, кто внес свой вклад!

30
ответ дан 07 сент. '12 в 15:45
источник

Добавлено мое предложение на 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

Explaination

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

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

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

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

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

View2 - это right_of View1

и

View3 справа_от View2

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

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

public FrameLayout getLeftLayout() {}

public FrameLayout getMiddleLayout() {}

public FrameLayout getRightLayout() {}

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


Ступень1

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

[View1] [_____ _____ View2] [_____ _____ View3]

Stage2

- OUT -! -------- IN Экран ------

[View1] [View2] [_____ _____ View3]

64
ответ дан 07 сент. '12 в 3:38
источник

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

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

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

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

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

13
ответ дан 19 февр. '13 в 19:51
источник

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

Начните с простой компоновки. Поскольку мы собираемся анимировать layout_weight, нам нужен LinearLayout как корневой вид для трех панелей.:

<?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>

Затем класс demo:

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/фрагмент самостоятельно, но по сравнению с попытками получить анимация работает красиво, это не похоже на плохой компромисс:)

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

6
ответ дан 06 сент. '12 в 18:44
источник

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

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

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

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

5
ответ дан 06 сент. '12 в 18:26
источник

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

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

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

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

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

В портретном режиме или на меньшем экране я использую немного другую компоновку и слайду Fragment 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 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;
}
2
ответ дан 06 сент. '12 в 22:25
источник