Внедрение эффекта KenBurns на Android с настройкой динамических растровых изображений

Фон

Я работаю над реализацией Эффект KenBurns "(demo здесь) на панели действий, как показано на этот образец библиотеки (за исключением значка, который перемещается, что я сделал сам).

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

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

Проблема

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

Вместо этого он просто выполняет масштабирование и перевод случайным образом, поэтому он может многократно увеличивать/уменьшать масштаб, и могут отображаться пустые пространства.

Код

Вот код, связанный с проблемами:

private float pickScale() {
    return MIN_SCALE_FACTOR + this.random.nextFloat() * (MAX_SCALE_FACTOR - MIN_SCALE_FACTOR);
}

private float pickTranslation(final int value, final float ratio) {
    return value * (ratio - 1.0f) * (this.random.nextFloat() - 0.5f);
}

public void animate(final ImageView view) {
    final float fromScale = pickScale();
    final float toScale = pickScale();
    final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
    final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);
    final float toTranslationX = pickTranslation(view.getWidth(), toScale);
    final float toTranslationY = pickTranslation(view.getHeight(), toScale);
    start(view, KenBurnsView.DELAY_BETWEEN_IMAGE_SWAPPING_IN_MS, fromScale, toScale, fromTranslationX,
            fromTranslationY, toTranslationX, toTranslationY);
}

И вот часть самой анимации, которая оживляет текущий ImageView:

private void start(View view, long duration, float fromScale, float toScale, float fromTranslationX, float fromTranslationY, float toTranslationX, float toTranslationY) {
    view.setScaleX(fromScale);
    view.setScaleY(fromScale);
    view.setTranslationX(fromTranslationX);
    view.setTranslationY(fromTranslationY);
    ViewPropertyAnimator propertyAnimator = view.animate().translationX(toTranslationX).translationY(toTranslationY).scaleX(toScale).scaleY(toScale).setDuration(duration);
    propertyAnimator.start();
}

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

Что я пробовал

Я работал с динамическими растровыми изображениями, но я не понимаю, что изменить на нем, чтобы он правильно обрабатывал размеры.

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

Вопрос

Что делать, чтобы правильно реализовать эффект Кена-Бернса, чтобы он мог обрабатывать динамически созданные растровые изображения?

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

Опять же, вопрос заключается не в получении растровых изображений или декодировании. Это о том, как заставить их работать с этим эффектом без сбоев или странного увеличения/уменьшения масштаба, которые показывают пустые места.

Ответы

Ответ 1

Я смотрю на исходный код KenBurnsView, и на самом деле не так сложно реализовать нужные функции, но есть несколько вещей, которые я должен прояснить первым:


1. Загрузка изображений динамически

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

Нетрудно загружать изображения динамически из Интернета, если вы знаете, что делаете, но есть много способов сделать это. Многие люди на самом деле не придумывают свое решение, но используют сетевую библиотеку, например Volley, чтобы загрузить изображение или они идут прямо на Picasso или что-то подобное. Лично я в основном использую свой собственный набор вспомогательных классов, но вам нужно решить, как именно вы хотите загрузить изображения. Использование библиотеки, например Picasso, скорее всего, лучшее решение для вас. В моих примерах кода в этом ответе будет использоваться библиотека Picasso, вот краткий пример использования Picasso:

Picasso.with(context).load("http://foo.com/bar.png").into(imageView);

2. Размер изображения

... и даже не смотрит размер входных изображений.

Я действительно не понимаю, что вы подразумеваете под этим. Внутри KenBurnsView используется ImageViews для отображения изображений. Они заботятся о правильном масштабировании и отображении изображения, и они, безусловно, учитывают размер изображений. Я думаю, что ваше замешательство может быть вызвано scaleType, которое установлено для ImageViews. Если вы посмотрите на файл макета R.layout.view_kenburns, который содержит макет KenBurnsView, вы увидите следующее:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image0"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ImageView
        android:id="@+id/image1"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

Обратите внимание, что для создания эффекта кроссфейда существует два ImageViews вместо одного. Важной частью является этот тег, который найден как на ImageViews:

android:scaleType="centerCrop"

Что это значит, скажите ImageView:

  • Центрировать изображение внутри ImageView
  • Масштабируйте изображение, чтобы его ширина располагалась внутри ImageView
  • Если изображение выше, чем ImageView, оно будет обрезано до размера ImageView

Таким образом, в текущем состоянии изображения внутри KenBurnsView могут быть обрезаны все время. Если вы хотите, чтобы изображение масштабировалось полностью внутри ImageView, поэтому ничего не нужно обрезать или удалить, вам нужно изменить scaleType на один из этих двух:

  • android:scaleType="fitCenter"
  • android:scaleType="centerInside"

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

ВАЖНО: изменение scaleType может помешать KenBurnsView!
Если вы действительно просто используете KenBurnsView для отображения двух изображений, то изменение scaleType не имеет значения, кроме того, как отображаются изображения, но если вы измените размер KenBurnsView - например, в анимации - и ImageViews установите scaleType на что-то другое, кроме centerCrop, вы потеряете эффект параллакса! Использование centerCrop в качестве scaleType для ImageView - это быстрый и простой способ создания эффектов, подобных параллаксу. Недостатком этого трюка является то, что вы заметили: Изображение в ImageView, скорее всего, будет обрезано и не будет полностью видимым!

Если вы посмотрите на макет, вы увидите, что все Views там имеют match_parent как layout_height и layout_width. Это также может быть проблемой для определенных изображений, поскольку ограничение match_parent и некоторые scaleTypes иногда приводят к странным результатам, когда изображения значительно меньше или больше, чем ImageView.


Анимация перевода также учитывает размер изображения - или, по крайней мере, размер ImageView. Если вы посмотрите на исходный код animate(...) и pickTranslation(...), вы увидите следующее:

// The value which is passed to pickTranslation() is the size of the View!
private float pickTranslation(final int value, final float ratio) {
    return value * (ratio - 1.0f) * (this.random.nextFloat() - 0.5f);
}

public void animate(final ImageView view) {
    final float fromScale = pickScale();
    final float toScale = pickScale();

    // Depending on the axis either the width or the height is passed to pickTranslation()
    final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
    final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);
    final float toTranslationX = pickTranslation(view.getWidth(), toScale);
    final float toTranslationY = pickTranslation(view.getHeight(), toScale);

    start(view, KenBurnsView.DELAY_BETWEEN_IMAGE_SWAPPING_IN_MS, fromScale, toScale, fromTranslationX, fromTranslationY, toTranslationX, toTranslationY);
}

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

Я могу придумать три основных способа исправить это:

  • Вместо рандомизации начальных и конечных значений вы просто производите начальные значения и выбрать конечные значения на основе начальных значений.
  • Вы сохраняете текущую стратегию рандомизации всех значений, но вы вводите ограничения диапазона для каждого значения. Например, fromScale должно быть случайным значением между 1.2f и 1.4f и toScale должно быть случайным значением между 1.6f и 1.8f.
  • Реализовать фиксированную трансляцию и масштабную анимацию (другими словами, скучный способ).

Если вы выберете подход №1 или №2, вам понадобится этот метод:

// Returns a random value between min and max
private float randomRange(float min, float max) {
    return random.nextFloat() * (max - min) + max;
}

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

public void animate(View view) {
    final float fromScale = randomRange(1.2f, 1.4f);
    final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
    final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);


    final float toScale =  randomRange(1.6f, 1.8f);
    final float toTranslationX = pickTranslation(view.getWidth(), toScale);
    final float toTranslationY = pickTranslation(view.getHeight(), toScale);

    start(view, this.mSwapMs, fromScale, toScale, fromTranslationX, fromTranslationY, toTranslationX, toTranslationY);
}

Как вы можете видеть, мне нужно только изменить способ вычисления fromScale и toScale, потому что значения переводов вычисляются из значений шкалы. Это не 100% -ное исправление, но это большое улучшение.


3. Решение №1: Фиксация KenBurnsView

(если возможно, используйте решение # 2)

Чтобы исправить KenBurnsView, вы можете реализовать предложения, упомянутые выше. Кроме того, нам необходимо реализовать способ добавления изображений динамически. Реализация способа обработки изображений KenBurnsView немного странная. Нам нужно немного изменить это. Поскольку мы используем Picasso, это будет довольно просто:

По существу вам просто нужно изменить метод swapImage(), я протестировал его так, и он работает:

private void swapImage() {
    if (this.urlList.size() > 0) {
        if(mActiveImageIndex == -1) {
            mActiveImageIndex = 1;
            animate(mImageViews[mActiveImageIndex]);
            return;
        }

        final int inactiveIndex = mActiveImageIndex;
        mActiveImageIndex = (1 + mActiveImageIndex) % mImageViews.length;
        Log.d(TAG, "new active=" + mActiveImageIndex);

        String url = this.urlList.get(this.urlIndex++);
        this.urlIndex = this.urlIndex % this.urlList.size();

        final ImageView activeImageView = mImageViews[mActiveImageIndex];
        activeImageView.setAlpha(0.0f);

        Picasso.with(this.context).load(url).into(activeImageView, new Callback() {

            @Override
            public void onSuccess() {
                ImageView inactiveImageView = mImageViews[inactiveIndex];

                animate(activeImageView);

                AnimatorSet animatorSet = new AnimatorSet();
                animatorSet.setDuration(mFadeInOutMs);
                animatorSet.playTogether(
                        ObjectAnimator.ofFloat(inactiveImageView, "alpha", 1.0f, 0.0f),
                        ObjectAnimator.ofFloat(activeImageView, "alpha", 0.0f, 1.0f)
                );
                animatorSet.start();
            }

            @Override
            public void onError() {
                Log.i(LOG_TAG, "Could not download next image");
            }
        });        
    }
}

Я пропустил несколько тривиальных частей, urlList - это всего лишь List<String>, который содержит все URL-адреса для изображений, которые мы хотим отобразить, urlIndex используется для циклического перехода через urlList. Я переместил анимацию в Callback. Таким образом, изображение будет загружено в фоновом режиме, и как только изображение будет успешно загружено, анимации будут воспроизводиться, а ImageViews будет перечеркнуто. Теперь можно удалить старый код из KenBurnsView, например, методы setResourceIds() или fillImageViews() теперь не нужны.


4. Решение №2: Лучше KenBurnsView + Picasso

Вторая библиотека, на которую вы ссылаетесь, этот, на самом деле содержит MUCH лучше KenBurnsView. KenBurnsView Я говорю об этом выше, это подкласс FrameLayout, и есть несколько проблем с подходом, который этот View принимает. KenBurnsView из вторая библиотека является подклассом ImageView, это уже значительное улучшение. Из-за этого мы можем использовать библиотеки загрузчика изображений, такие как Picasso непосредственно на KenBurnsView, и нам не нужно позаботьтесь обо всем. Вы говорите, что у вас возникли случайные сбои с вторая библиотека? Я тестировал его довольно широко последние несколько часов и не сталкивался с одним сбоем.

С KenBurnsView из вторая библиотека и Picasso все это становится очень простым и очень мало строк кода, вам просто нужно создать KenBurnsView, например, в xml:

<com.flaviofaria.kenburnsview.KenBurnsView
    android:id="@+id/kbvExample"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/image" />

И затем в вашем Fragment вам сначала нужно найти представление в макете, а затем в onViewCreated() мы загрузим изображение с помощью Picasso:

private KenBurnsView kbvExample;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_kenburns_test, container, false);

    this.kbvExample = (KenBurnsView) view.findViewById(R.id.kbvExample);

    return view;
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    Picasso.with(getActivity()).load(IMAGE_URL).into(this.kbvExample);
}

5. Тестирование

Я тестировал все на своем Nexus 5 под управлением Android 4.4.2. Поскольку ViewPropertyAnimators используются, все это должно быть совместимо где-то вплоть до уровня API 16, возможно даже 12.

Я пропустил несколько строк кода здесь и там, поэтому, если у вас есть какие-либо вопросы, не стесняйтесь спрашивать!