android - usar - ¿Cómo animar ImageView desde el centro-recorte para llenar la pantalla y viceversa(estilo facebook)?
ver pantalla de android en pc sin app (7)
Fondo
La aplicación de Facebook tiene una buena animación de transición entre una imagen pequeña en una publicación y un modo ampliado de la misma que el usuario también puede ampliar.
Como lo veo, la animación no solo amplía y mueve la imageView de acuerdo con su ubicación y tamaño anteriores, sino que también revela contenido en lugar de estirar el contenido de la imageView.
Esto se puede ver usando el siguiente boceto que he hecho:
La pregunta
¿Cómo lo hicieron? ¿Realmente tenían 2 vistas animando para revelar el contenido?
¿Cómo lo hicieron tan fluido como si fuera una vista única?
el único tutorial que he visto (enlace here ) de una imagen que se amplía a pantalla completa no se muestra bien cuando la miniatura está configurada como centro-recorte.
No solo eso, sino que funciona incluso en API baja de Android.
¿Alguien sabe de una biblioteca que tenga una habilidad similar?
EDITAR: Encontré un camino y publiqué una respuesta , pero se basa en el cambio de layoutParams, y creo que no es eficiente y recomendable.
He intentado usar las animaciones normales y otros trucos de animación, pero por ahora eso es lo único que me funcionó.
Si alguien sabe qué hacer para que funcione mejor, escríbalo.
Creo que la forma más fácil es animar la altura de ImageView (una vista de imagen regular, no necesariamente una vista personalizada) mientras mantiene el tipo de escala a centrado vertical hasta la altura máxima, lo que puede saber de antemano si configura la altura de la imagen en wrap_content en su diseño y luego use un ViewTreeObserver para saber cuándo ha terminado el diseño, para que pueda obtener la altura de ImageView y luego establecer la nueva altura "contraída". No lo he probado, pero así es como lo haría.
También puedes echarle un vistazo a esta publicación, hacen algo similar http://nerds.airbnb.com/host-experience-android/
Encontré una manera de obtener un efecto similar en un prototipo rápido. Puede que no sea adecuado para uso de producción (todavía estoy investigando), pero es rápido y fácil.
Use una transición de atenuación en su actividad / transición de fragmentos (que comienza con ImageView en exactamente la misma posición). La versión del fragmento:
final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ...etc
La versión de la actividad:
Intent intent = new Intent(context, MyDetailActivity.class); startActivity(intent); getActivity().overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
Esto da una transición suave sin un parpadeo.
Ajuste los diseños dinámicamente en el onStart () del nuevo fragmento (debe guardar los campos de los miembros en las partes apropiadas de su interfaz de usuario en onCreateView y agregar algunas marcas para asegurarse de que solo se llame una vez a este código).
@Override public void onStart() { super.onStart(); // Remove the padding on any layouts that the image view is inside mMainLayout.setPadding(0, 0, 0, 0); // Get the screen size using a utility method, e.g. // http://.com/a/12082061/112705 // then work out your desired height, e.g. using the image aspect ratio. int desiredHeight = (int) (screenWidth * imgAspectRatio); // Resize the image to fill the whole screen width, removing // any layout margins that it might have (you may need to remove // padding too) LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(screenWidth, desiredHeight); layoutParams.setMargins(0, 0, 0, 0); mImageView.setLayoutParams(layoutParams); }
Hice un código de muestra en Github .
La clave de este código es usar canvas.clipRect()
. Pero, solo funciona cuando la vista CroppedImageview
es match_parent
.
Para explicarlo simplemente, dejo la animación de escala y traducción a ViewPropertyAnimator
. Entonces, puedo centrarme en recortar la imagen.
Al igual que en la imagen anterior, calcule la región de recorte y cambie la región del clip al tamaño de vista final.
Controlador de animacion
class ZoomAnimationController(private val view: CroppedImageView, startRect: Rect, private val viewRect: Rect, imageSize: Size) {
companion object {
const val DURATION = 300L
}
private val startViewRect: RectF
private val scale: Float
private val startClipRect: RectF
private val animatingRect: Rect
private var cropAnimation: ValueAnimator? = null
init {
val startImageRect = getProportionalRect(startRect, imageSize, ImageView.ScaleType.CENTER_CROP)
startViewRect = getProportionalRect(startImageRect, viewRect.getSize(), ImageView.ScaleType.CENTER_CROP)
scale = startViewRect.width() / viewRect.width()
val finalImageRect = getProportionalRect(viewRect, imageSize, ImageView.ScaleType.FIT_CENTER)
startClipRect = getProportionalRect(finalImageRect, startRect.getSize() / scale, ImageView.ScaleType.FIT_CENTER)
animatingRect = Rect()
startClipRect.round(animatingRect)
}
fun init() {
view.x = startViewRect.left
view.y = startViewRect.top
view.pivotX = 0f
view.pivotY = 0f
view.scaleX = scale
view.scaleY = scale
view.setClipRegion(animatingRect)
}
fun startAnimation() {
cropAnimation = createCropAnimator().apply {
start()
}
view.animate()
.x(0f)
.y(0f)
.scaleX(1f)
.scaleY(1f)
.setDuration(DURATION)
.start()
}
private fun createCropAnimator(): ValueAnimator {
return ValueAnimator.ofFloat(0f, 1f).apply {
duration = DURATION
addUpdateListener {
val weight = animatedValue as Float
animatingRect.set(
(startClipRect.left * (1 - weight) + viewRect.left * weight).toInt(),
(startClipRect.top * (1 - weight) + viewRect.top * weight).toInt(),
(startClipRect.right * (1 - weight) + viewRect.right * weight).toInt(),
(startClipRect.bottom * (1 - weight) + viewRect.bottom * weight).toInt()
)
Log.d("SSO", "animatingRect=$animatingRect")
view.setClipRegion(animatingRect)
}
}
}
private fun getProportionalRect(viewRect: Rect, imageSize: Size, scaleType: ImageView.ScaleType): RectF {
return getProportionalRect(RectF(viewRect), imageSize, scaleType)
}
private fun getProportionalRect(viewRect: RectF, imageSize: Size, scaleType: ImageView.ScaleType): RectF {
val viewRatio = viewRect.height() / viewRect.width()
if ((scaleType == ImageView.ScaleType.FIT_CENTER && viewRatio > imageSize.ratio)
|| (scaleType == ImageView.ScaleType.CENTER_CROP && viewRatio <= imageSize.ratio)) {
val width = viewRect.width()
val height = width * imageSize.ratio
val paddingY = (height - viewRect.height()) / 2f
return RectF(viewRect.left, viewRect.top - paddingY, viewRect.right, viewRect.bottom + paddingY)
} else if ((scaleType == ImageView.ScaleType.FIT_CENTER && viewRatio <= imageSize.ratio)
|| (scaleType == ImageView.ScaleType.CENTER_CROP && viewRatio > imageSize.ratio)){
val height = viewRect.height()
val width = height / imageSize.ratio
val paddingX = (width - viewRect.width()) / 2f
return RectF(viewRect.left - paddingX, viewRect.top, viewRect.right + paddingX, viewRect.bottom)
}
return RectF()
}
CroppedImageView
override fun onDraw(canvas: Canvas?) {
if (clipRect.width() > 0 && clipRect.height() > 0) {
canvas?.clipRect(clipRect)
}
super.onDraw(canvas)
}
fun setClipRegion(rect: Rect) {
clipRect.set(rect)
invalidate()
}
solo funciona cuando CroppedImageview es match_parent, porque
- Las rutas de inicio a fin se incluyen en CroppedImageView. Si no, la animación no se muestra. Por lo tanto, hacer que su tamaño sea match_parent es fácil de pensar.
- No implementé el código para casos especiales ...
No estoy seguro de por qué todo el mundo está hablando sobre el marco. Usar el código de otras personas puede ser bueno a veces; pero parece que lo que buscas es un control preciso sobre el aspecto. Al acceder al contexto de los gráficos, puedes tener eso. La tarea es bastante simple en cualquier entorno que tenga un contexto gráfico. En Android, puedes obtenerlo anulando el método onDraw
y utilizando el objeto Canvas. Tiene todo lo que necesita para dibujar una imagen en diferentes escalas, posiciones y recortes. Incluso puedes usar una matriz si estás familiarizado con ese tipo de cosas.
Pasos
Asegúrese de tener un control exacto del posicionamiento, la escala y el clip. Esto significa deshabilitar cualquier diseño o alineación automática que pueda configurarse dentro del contenedor de objetos.
Calcule cuál será su parámetro
t
para la interpolación lineal y cómo querrá que se relacione con el tiempo. ¿Qué tan rápido o lento, y habrá alguna relajación. Debería depender del tiempo.Después de que las miniaturas se almacenan en caché, cargue la imagen a escala completa en el fondo. Pero no lo muestres todavía.
Cuando se dispare el activador de animación, muestre la imagen grande y conduzca su animación con su parámetro
t
utilizando la interpolación entre los estados de propiedades iniciales y estados de propiedades finales. Haga esto para las tres propiedades, posición, escala y clip. Así que para todas las propiedades haga lo siguiente:Sinterpolated = Sinitial * (t-1) + Sfinal * t; // where // t is between 0.0 and 1.0 // and S is the states value // for every part of scale, position, and clip // // Sinitial is what you are going from // Sfinal is what you are going to // // t should change from 0.0->1.0 in // over time anywhere from 12/sec or 60/sec.
Si todas las propiedades están controladas por el mismo parámetro, la animación será suave. Como un bono adicional, aquí hay un consejo para el tiempo. Siempre que pueda mantener su parámetro t
entre 0 y 1, la aceleración hacia adentro o hacia afuera puede ser hackeada con una línea de código:
// After your t is all setup
t = t * t; // for easing in
// or
t = Math.sqrt(t); // for easing out
Ok, he encontrado una forma posible de hacerlo. He hecho layoutParams como variables que siguen cambiando usando el ObjectAnimator de la librería nineOldAndroids . Creo que no es la mejor manera de lograrlo, ya que causa mucho OnDraw y onLayout, pero si el contenedor tiene solo unas pocas vistas y no cambia su tamaño, tal vez esté bien.
el supuesto es que el imageView que animo tomará el tamaño exacto que se necesita al final, y que (actualmente) tanto la miniatura como el animado imageView tienen el mismo contenedor (pero debería ser fácil cambiarlo).
como he probado, también es posible agregar funciones de zoom al extender la clase TouchImageView . acaba de establecer el tipo de escala al principio para recortar el centro, y cuando finaliza la animación, vuelva a establecerlo en la matriz y, si lo desea, puede configurar los Paquetes de diseño para que llenen todo el contenedor (y establecer el margen en 0,0 ).
También me pregunto por qué el AnimatorSet no funcionó para mí, por lo que mostraré aquí algo que funciona, con la esperanza de que alguien pueda decirme qué debo hacer.
Aquí está el código:
MainActivity.java
public class MainActivity extends Activity {
private static final int IMAGE_RES_ID = R.drawable.test_image_res_id;
private static final int ANIM_DURATION = 5000;
private final Handler mHandler = new Handler();
private ImageView mThumbnailImageView;
private CustomImageView mFullImageView;
private Point mFitSizeBitmap;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFullImageView = (CustomImageView) findViewById(R.id.fullImageView);
mThumbnailImageView = (ImageView) findViewById(R.id.thumbnailImageView);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
prepareAndStartAnimation();
}
}, 2000);
}
private void prepareAndStartAnimation() {
final int thumbX = mThumbnailImageView.getLeft(), thumbY = mThumbnailImageView.getTop();
final int thumbWidth = mThumbnailImageView.getWidth(), thumbHeight = mThumbnailImageView.getHeight();
final View container = (View) mFullImageView.getParent();
final int containerWidth = container.getWidth(), containerHeight = container.getHeight();
final Options bitmapOptions = getBitmapOptions(getResources(), IMAGE_RES_ID);
mFitSizeBitmap = getFitSize(bitmapOptions.outWidth, bitmapOptions.outHeight, containerWidth, containerHeight);
mThumbnailImageView.setVisibility(View.GONE);
mFullImageView.setVisibility(View.VISIBLE);
mFullImageView.setContentWidth(thumbWidth);
mFullImageView.setContentHeight(thumbHeight);
mFullImageView.setContentX(thumbX);
mFullImageView.setContentY(thumbY);
runEnterAnimation(containerWidth, containerHeight);
}
private Point getFitSize(final int width, final int height, final int containerWidth, final int containerHeight) {
int resultHeight, resultWidth;
resultHeight = height * containerWidth / width;
if (resultHeight <= containerHeight) {
resultWidth = containerWidth;
} else {
resultWidth = width * containerHeight / height;
resultHeight = containerHeight;
}
return new Point(resultWidth, resultHeight);
}
public void runEnterAnimation(final int containerWidth, final int containerHeight) {
final ObjectAnimator widthAnim = ObjectAnimator.ofInt(mFullImageView, "contentWidth", mFitSizeBitmap.x)
.setDuration(ANIM_DURATION);
final ObjectAnimator heightAnim = ObjectAnimator.ofInt(mFullImageView, "contentHeight", mFitSizeBitmap.y)
.setDuration(ANIM_DURATION);
final ObjectAnimator xAnim = ObjectAnimator.ofInt(mFullImageView, "contentX",
(containerWidth - mFitSizeBitmap.x) / 2).setDuration(ANIM_DURATION);
final ObjectAnimator yAnim = ObjectAnimator.ofInt(mFullImageView, "contentY",
(containerHeight - mFitSizeBitmap.y) / 2).setDuration(ANIM_DURATION);
widthAnim.start();
heightAnim.start();
xAnim.start();
yAnim.start();
// TODO check why using AnimatorSet doesn''t work here:
// final com.nineoldandroids.animation.AnimatorSet set = new AnimatorSet();
// set.playTogether(widthAnim, heightAnim, xAnim, yAnim);
}
public static BitmapFactory.Options getBitmapOptions(final Resources res, final int resId) {
final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, bitmapOptions);
return bitmapOptions;
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.example.facebookstylepictureanimationtest.CustomImageView
android:id="@+id/fullImageView"
android:layout_width="0px"
android:layout_height="0px"
android:background="#33ff0000"
android:scaleType="centerCrop"
android:src="@drawable/test_image_res_id"
android:visibility="invisible" />
<ImageView
android:id="@+id/thumbnailImageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:scaleType="centerCrop"
android:src="@drawable/test_image_res_id" />
</RelativeLayout>
CustomImageView.java
public class CustomImageView extends ImageView {
public CustomImageView(final Context context) {
super(context);
}
public CustomImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
public void setContentHeight(final int contentHeight) {
final LayoutParams layoutParams = getLayoutParams();
layoutParams.height = contentHeight;
setLayoutParams(layoutParams);
}
public void setContentWidth(final int contentWidth) {
final LayoutParams layoutParams = getLayoutParams();
layoutParams.width = contentWidth;
setLayoutParams(layoutParams);
}
public int getContentHeight() {
return getLayoutParams().height;
}
public int getContentWidth() {
return getLayoutParams().width;
}
public int getContentX() {
return ((MarginLayoutParams) getLayoutParams()).leftMargin;
}
public void setContentX(final int contentX) {
final MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = contentX;
setLayoutParams(layoutParams);
}
public int getContentY() {
return ((MarginLayoutParams) getLayoutParams()).topMargin;
}
public void setContentY(final int contentY) {
final MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.topMargin = contentY;
setLayoutParams(layoutParams);
}
}
Otra solución, si solo desea hacer una animación de una imagen de pequeña a grande, puede probar ActivityOptions.makeThumbnailScaleUpAnimation o makeScaleUpAnimation y ver si le convienen.
Puedes lograr esto a través de Transition
Api, y este es el siguiente gif:
Código esencial a continuación:
private void zoomIn() {
ViewGroup.LayoutParams layoutParams = mImage.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
layoutParams.width = (int) (width * 2);
layoutParams.height = height * 2;
mImage.setLayoutParams(layoutParams);
mImage.setScaleType(ImageView.ScaleType.FIT_CENTER);
TransitionSet transitionSet = new TransitionSet();
Transition bound = new ChangeBounds();
transitionSet.addTransition(bound);
Transition changeImageTransform = new ChangeImageTransform();
transitionSet.addTransition(changeImageTransform);
transitionSet.setDuration(1000);
TransitionManager.beginDelayedTransition(mRootView, transitionSet);
}
versión sdk> = 21