Ответ 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.
Я пропустил несколько строк кода здесь и там, поэтому, если у вас есть какие-либо вопросы, не стесняйтесь спрашивать!