Implementation of KenBurns effect on Android with dynamic bitmaps setting

Фон

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

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

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

Эта проблема

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

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

Код

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

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.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 чтобы:

  1. Центрировать изображение внутри ImageView
  2. Масштабируйте изображение так, чтобы его ширина вписывалась в ImageView
  3. Если изображение выше, чем 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);
}

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

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

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

Выберете ли вы подход № 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 + Пикассо

Вторая библиотека, на которую вы ссылаетесь, эта, на самом деле, НАМНОГО лучше 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() загружаем изображение с Пикассо:

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.

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

Другие вопросы по тегам