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