Implementación del efecto KenBurns en Android con configuración dinámica de mapas de bits
animation android-animation (1)
He KenBurnsView
el código fuente de KenBurnsView
y en realidad no es tan difícil implementar las funciones que desea, pero hay algunas cosas que debo aclarar primero:
1. Cargando imágenes dinámicamente.
La implementación actual no puede manejar múltiples mapas de bits que se dan dinámicamente (desde Internet, por ejemplo), ...
No es difícil descargar imágenes dinámicamente de Internet si sabes lo que estás haciendo, pero hay muchas maneras de hacerlo. Muchas personas en realidad no tienen su propia solución, sino que usan una biblioteca de redes como Volley para descargar la imagen o van directamente a Picasso o algo similar. Personalmente, uso principalmente mi propio conjunto de clases de ayuda, pero usted tiene que decidir cómo desea descargar exactamente las imágenes. Usar una biblioteca como Picasso es probablemente la mejor solución para ti. Los ejemplos de mi código en esta respuesta usarán la biblioteca de Picasso , aquí hay un ejemplo rápido de cómo usar Picasso :
Picasso.with(context).load("http://foo.com/bar.png").into(imageView);
2. Tamaño de la imagen
... y ni siquiera mira el tamaño de las imágenes de entrada.
Realmente no entiendo lo que quieres decir con eso. Internamente, KenBurnsView
utiliza ImageViews
para mostrar las imágenes. Se encargan de escalar y mostrar la imagen correctamente y, sin duda, tienen en cuenta el tamaño de las imágenes. Creo que su confusión puede ser causada por el tipo de scaleType
que se establece para las ImageViews
. Si observa el archivo de diseño R.layout.view_kenburns
que contiene el diseño de KenBurnsView
, verá esto:
<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>
Tenga en cuenta que existen dos ImageViews
lugar de solo una para crear el efecto de fundido cruzado. La parte importante es esta etiqueta que se encuentra en ambos ImageViews
:
android:scaleType="centerCrop"
Lo que esto hace es decirle a ImageView
que:
- Centrar la imagen dentro del
ImageView
- Escala la imagen para que su ancho se ajuste dentro de la
ImageView
- Si la imagen es más alta que la
ImageView
, se recortará al tamaño de laImageView
Por lo tanto, en su estado actual, las imágenes dentro de KenBurnsView
pueden recortarse en todo momento. Si desea que la imagen se scaleType
para que se ajuste completamente dentro de ImageView
modo que no haya que recortar ni eliminar nada, debe cambiar el tipo de scaleType
a uno de esos dos:
-
android:scaleType="fitCenter"
-
android:scaleType="centerInside"
No recuerdo la diferencia exacta entre esos dos, pero ambos deberían tener el efecto deseado de escalar la imagen para que se ajuste a los ejes X e Y dentro de ImageView
mientras que al mismo tiempo la centran dentro de ImageView
.
IMPORTANTE: ¡Cambiar el tipo de scaleType
desordena potencialmente el KenBurnsView
!
Si realmente solo usa KenBurnsView
para mostrar dos imágenes, cambiar el tipo de scaleType
no importará de cómo se muestren las imágenes, pero si KenBurnsView
el tamaño de KenBurnsView
, por ejemplo, en una animación, y las scaleType
tienen el tipo de scaleType
ajustado a otro. de centerCrop
el efecto de paralaje! Usar centerCrop
como scaleType
de un ImageView
es una manera rápida y fácil de crear efectos similares a los de paralaje. El inconveniente de este truco es probablemente lo que notó: ¡ la imagen en ImageView
probablemente será recortada y no completamente visible!
Si miras el diseño, puedes ver que todas las Views
tienen match_parent
como layout_height
y layout_width
. Esto también podría ser un problema para ciertas imágenes, como la restricción match_parent
y ciertos scaleTypes
veces producen resultados extraños cuando las imágenes son considerablemente más pequeñas o más grandes que ImageView
.
La animación de traducción también tiene en cuenta el tamaño de la imagen, o al menos el tamaño de ImageView
. Si observa el código fuente de animate(...)
y pickTranslation(...)
verá esto:
// 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);
}
Por lo tanto, la vista ya tiene en cuenta el tamaño de las imágenes y cuánto se escala la imagen al calcular la traducción. Entonces, el concepto de cómo funciona esto está bien, el único problema que veo es que tanto los valores iniciales como los finales son aleatorios sin ninguna dependencia entre esos dos valores. Lo que esto significa es una cosa simple: el inicio y el punto final de la animación pueden ser exactamente la misma posición o pueden estar muy cerca uno del otro. Como resultado de esto, la animación a veces puede ser muy significativa y otras apenas perceptible.
Puedo pensar en tres maneras principales de arreglar eso:
- En lugar de asignar al azar los valores de inicio y finalización, simplemente se asignan al azar los valores de inicio y se seleccionan los valores finales según los valores de inicio.
- Mantiene la estrategia actual de aleatorizar todos los valores, pero impone restricciones de rango en cada valor. Por ejemplo,
fromScale
debe ser un valor aleatorio entre1.2f
y1.4f
ytoScale
debe ser un valor aleatorio entre1.6f
y1.8f
. - Implementar una traducción fija y una animación a escala (En otras palabras, la forma aburrida).
Ya sea que elija el enfoque # 1 o # 2, necesitará este método:
// Returns a random value between min and max
private float randomRange(float min, float max) {
return random.nextFloat() * (max - min) + max;
}
Aquí he modificado el método animate()
para forzar una cierta distancia entre los puntos de inicio y final de la animación:
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);
}
Como puede ver, solo necesito modificar cómo se calculan desde Escala y hasta toScale
porque los valores de traducción se calculan a partir de los valores de escala. Esto no es una solución al 100%, pero es una gran mejora.
3. Solución # 1: Arreglando KenBurnsView
(Use la solución # 2 si es posible)
Para arreglar el KenBurnsView
puede implementar las sugerencias que mencioné anteriormente. Adicionalmente, necesitamos implementar una forma para que las imágenes se agreguen dinámicamente. La implementación de cómo KenBurnsView
maneja las imágenes es un poco rara. Vamos a necesitar modificar eso un poco. Ya que estamos usando Picasso esto va a ser bastante simple:
Esencialmente, solo necesitas modificar el método swapImage()
, lo probé así y está funcionando:
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");
}
});
}
}
He omitido algunas partes triviales, urlList
es solo una List<String>
que contiene todas las direcciones URL de las imágenes que queremos mostrar, urlIndex
se usa para recorrer la lista urlList
. Moví la animación al Callback
. De ese modo, la imagen se descargará en segundo plano y, tan pronto como la imagen se haya descargado correctamente, se reproducirán las animaciones y se reproducirán las ImageViews
cruzadas. Ahora se puede eliminar gran parte del código antiguo de KenBurnsView
, por ejemplo, los métodos setResourceIds()
o fillImageViews()
ahora son innecesarios.
4. Solución # 2: Mejor KenBurnsView
+ Picasso
La segunda biblioteca a la que te KenBurnsView
, here , en realidad contiene un KenBurnsView
MUCHO mejor. El KenBurnsView
que KenBurnsView
anteriormente es una subclase de FrameLayout
y hay algunos problemas con el enfoque que toma esta View
. El KenBurnsView
de la here es una subclase de ImageView
, esto ya es una gran mejora. Debido a esto, podemos usar bibliotecas de cargadores de imágenes como Picasso directamente en KenBurnsView
y no tenemos que ocuparnos de nada por nosotros mismos. ¿Dices que experimentas choques aleatorios con la here ? Lo he estado probando bastante extensivamente en las últimas horas y no encontré un solo fallo.
Con KenBurnsView
de la here y Picasso todo esto se vuelve muy fácil y muy pocas líneas de código, solo tiene que crear un KenBurnsView
por ejemplo, en xml:
<com.flaviofaria.kenburnsview.KenBurnsView
android:id="@+id/kbvExample"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/image" />
Y luego, en tu Fragment
, primero debes encontrar la vista en el diseño y luego en onViewCreated()
la imagen con 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. Pruebas
He probado todo en mi Nexus 5 con Android 4.4.2. Dado que se utilizan ViewPropertyAnimators
, todo esto debería ser compatible en algún lugar hasta el Nivel 16 de API, incluso 12
He omitido algunas líneas de código aquí y allá, así que si tiene alguna pregunta, ¡no dude en preguntar!
Fondo
Estoy trabajando en una implementación del " efecto KenBurns " (demostración here ) en la barra de acción, como se muestra en la muestra de esta biblioteca (excepto el icono que se mueve, que yo mismo he hecho).
De hecho, incluso lo pregunté hace mucho tiempo ( here ), que en este momento ni siquiera sabía su nombre. Estaba seguro de haber encontrado una solución, pero tiene algunos problemas.
Además, dado que a veces muestro las imágenes del dispositivo, algunas de ellas incluso necesitan ser rotadas, por lo que uso un GirableDrawable (como se muestra here ).
El problema
La implementación actual no puede manejar múltiples mapas de bits que se dan dinámicamente (desde Internet, por ejemplo), y ni siquiera mira el tamaño de las imágenes de entrada.
En su lugar, solo hace el zoom y la traducción de forma aleatoria, muchas veces puede hacer un zoom demasiado grande o pequeño, y se pueden mostrar espacios vacíos.
El código
Aquí está el código que está relacionado con los problemas:
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);
}
Y aquí está la parte de la animación en sí, que anima el actual 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();
}
Como puede ver, esto no se ve en los tamaños de vista / mapa de bits, y solo selecciona aleatoriamente cómo hacer zoom y desplazarse.
Lo que he intentado
Lo hice funcionar con mapas de bits dinámicos, pero no entiendo qué cambiar en él para que pueda manejar los tamaños correctamente.
También he notado que hay otra biblioteca ( here ) que hace este trabajo, pero también tiene los mismos problemas, y es aún más difícil entender cómo solucionarlos allí. Además se bloquea aleatoriamente. Here''s una publicación que he informado al respecto.
La pregunta
¿Qué se debe hacer para implementar el efecto Ken-Burns correctamente, de modo que pueda manejar mapas de bits creados dinámicamente?
Pienso que tal vez la mejor solución sea personalizar la forma en que ImageView dibuja su contenido, para que, en cualquier momento, muestre una parte del mapa de bits que se le entrega, y la animación real esté entre dos rectángulos. del mapa de bits. Lamentablemente, no estoy seguro de cómo hacer esto.
Una vez más, la pregunta no es acerca de obtener mapas de bits o decodificación. Se trata de cómo hacer que funcionen bien con este efecto sin bloqueos o acercamientos / alejamientos extraños que muestren espacios vacíos.