Tutorial ini menunjukkan cara melakukan animasi sentuh-untuk-zoom, yang berguna untuk aplikasi seperti galeri foto untuk menganimasikan tampilan dari thumbnail ke gambar ukuran penuh yang mengisi layar.
Seperti inilah tampilan animasi sentuh-untuk-zoom yang memperluas thumbnail gambar untuk mengisi layar:
Jika Anda ingin langsung melihat contoh yang berfungsi penuh, lihat class UIAnimation
dari project Contoh Speaker Wear di GitHub.
Membuat tampilan
Buat file tata letak yang berisi versi kecil dan besar dari konten yang ingin diperbesar. Contoh berikut membuat ImageButton
untuk thumbnail gambar yang dapat diklik dan ImageView
yang menampilkan tampilan gambar yang diperbesar:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <ImageButton android:id="@+id/thumb_button_1" android:layout_width="100dp" android:layout_height="75dp" android:layout_marginRight="1dp" android:src="@drawable/thumb1" android:scaleType="centerCrop" android:contentDescription="@string/description_image_1" /> </LinearLayout> <!-- This initially-hidden ImageView will hold the expanded/zoomed version of the images above. Without transformations applied, it takes up the entire screen. To achieve the "zoom" animation, this view's bounds are animated from the bounds of the thumbnail button above, to its final laid-out bounds. --> <ImageView android:id="@+id/expanded_image" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" android:contentDescription="@string/description_zoom_touch_close" /> </FrameLayout>
Menyiapkan animasi zoom
Setelah Anda menerapkan tata letak, siapkan pengendali peristiwa yang memicu animasi zoom.
Contoh berikut menambahkan View.OnClickListener
ke ImageButton
untuk menjalankan animasi zoom saat pengguna mengklik tombol gambar:
Kotlin
class ZoomActivity : FragmentActivity() { // Hold a reference to the current animator, // so that it can be canceled mid-way. private var currentAnimator: Animator? = null // The system "short" animation time duration, in milliseconds. This // duration is ideal for subtle animations or animations that occur // very frequently. private var shortAnimationDuration: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_zoom) // Hook up clicks on the thumbnail views. val thumb1View: View = findViewById(R.id.thumb_button_1) thumb1View.setOnClickListener({ zoomImageFromThumb(thumb1View, R.drawable.image1) }) // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) } ... }
Java
public class ZoomActivity extends FragmentActivity { // Hold a reference to the current animator, // so that it can be canceled mid-way. private Animator currentAnimator; // The system "short" animation time duration, in milliseconds. This // duration is ideal for subtle animations or animations that occur // very frequently. private int shortAnimationDuration; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_zoom); // Hook up clicks on the thumbnail views. final View thumb1View = findViewById(R.id.thumb_button_1); thumb1View.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { zoomImageFromThumb(thumb1View, R.drawable.image1); } }); // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = getResources().getInteger( android.R.integer.config_shortAnimTime); } ... }
Memperbesar tampilan
Sekarang Anda perlu menganimasikan dari tampilan berukuran normal ke tampilan yang diperbesar jika sesuai. Secara umum, Anda perlu menganimasikan dari batas tampilan berukuran normal ke batas tampilan berukuran lebih besar. Metode berikut ini menunjukkan kepada Anda cara mengimplementasikan animasi zoom yang memperbesar dari thumbnail gambar ke tampilan yang diperbesar dengan melakukan hal-hal berikut:
- Tetapkan gambar beresolusi tinggi ke
ImageView
"zoomed-in" (diperbesar) yang tersembunyi. Contoh berikut memuat resource gambar besar di UI thread demi kemudahan. Anda mungkin ingin melakukan pemuatan ini di thread terpisah untuk mencegah pemblokiran pada UI thread, lalu menyetel bitmap pada UI thread. Idealnya, bitmap tidak boleh lebih besar dari ukuran layar. - Hitung batas awal dan akhir untuk
ImageView
. - Animasikan masing-masing empat properti penempatan dan pengukuran
,X
, (Y
SCALE_X
, dan
) secara bersamaan, dari batas awal hingga batas akhir. Keempat animasi ini ditambahkan keSCALE_Y
AnimatorSet
sehingga animasi tersebut dapat dimulai pada waktu yang sama. - Perkecil kembali dengan menjalankan animasi serupa tetapi secara terbalik jika pengguna menyentuh layar saat gambar diperbesar. Anda dapat melakukannya dengan menambahkan
View.OnClickListener
keImageView
. Saat diklik,ImageView
akan mengecilkan kembali ke ukuran thumbnail gambar dan menetapkan visibilitasnya keGONE
untuk menyembunyikannya.
Kotlin
private fun zoomImageFromThumb(thumbView: View, imageResId: Int) { // If there's an animation in progress, cancel it // immediately and proceed with this one. currentAnimator?.cancel() // Load the high-resolution "zoomed-in" image. val expandedImageView: ImageView = findViewById(R.id.expanded_image) expandedImageView.setImageResource(imageResId) // Calculate the starting and ending bounds for the zoomed-in image. // This step involves lots of math. Yay, math. val startBoundsInt = Rect() val finalBoundsInt = Rect() val globalOffset = Point() // The start bounds are the global visible rectangle of the thumbnail, // and the final bounds are the global visible rectangle of the container // view. Also set the container view's offset as the origin for the // bounds, since that's the origin for the positioning animation // properties (X, Y). thumbView.getGlobalVisibleRect(startBoundsInt) findViewById<View>(R.id.container) .getGlobalVisibleRect(finalBoundsInt, globalOffset) startBoundsInt.offset(-globalOffset.x, -globalOffset.y) finalBoundsInt.offset(-globalOffset.x, -globalOffset.y) val startBounds = RectF(startBoundsInt) val finalBounds = RectF(finalBoundsInt) // Adjust the start bounds to be the same aspect ratio as the final // bounds using the "center crop" technique. This prevents undesirable // stretching during the animation. Also calculate the start scaling // factor (the end scaling factor is always 1.0). val startScale: Float if ((finalBounds.width() / finalBounds.height() > startBounds.width() / startBounds.height())) { // Extend start bounds horizontally startScale = startBounds.height() / finalBounds.height() val startWidth: Float = startScale * finalBounds.width() val deltaWidth: Float = (startWidth - startBounds.width()) / 2 startBounds.left -= deltaWidth.toInt() startBounds.right += deltaWidth.toInt() } else { // Extend start bounds vertically startScale = startBounds.width() / finalBounds.width() val startHeight: Float = startScale * finalBounds.height() val deltaHeight: Float = (startHeight - startBounds.height()) / 2f startBounds.top -= deltaHeight.toInt() startBounds.bottom += deltaHeight.toInt() } // Hide the thumbnail and show the zoomed-in view. When the animation // begins, it will position the zoomed-in view in the place of the // thumbnail. thumbView.alpha = 0f expandedImageView.visibility = View.VISIBLE // Set the pivot point for SCALE_X and SCALE_Y transformations // to the top-left corner of the zoomed-in view (the default // is the center of the view). expandedImageView.pivotX = 0f expandedImageView.pivotY = 0f // Construct and run the parallel animation of the four translation and // scale properties (X, Y, SCALE_X, and SCALE_Y). currentAnimator = AnimatorSet().apply { play(ObjectAnimator.ofFloat( expandedImageView, View.X, startBounds.left, finalBounds.left) ).apply { with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top)) with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f)) with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f)) } duration = shortAnimationDuration.toLong() interpolator = DecelerateInterpolator() addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { currentAnimator = null } override fun onAnimationCancel(animation: Animator) { currentAnimator = null } }) start() } // Upon clicking the zoomed-in image, it should zoom back down // to the original bounds and show the thumbnail instead of // the expanded image. expandedImageView.setOnClickListener { currentAnimator?.cancel() // Animate the four positioning/sizing properties in parallel, // back to their original values. currentAnimator = AnimatorSet().apply { play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left)).apply { with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top)) with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale)) with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale)) } duration = shortAnimationDuration.toLong() interpolator = DecelerateInterpolator() addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { thumbView.alpha = 1f expandedImageView.visibility = View.GONE currentAnimator = null } override fun onAnimationCancel(animation: Animator) { thumbView.alpha = 1f expandedImageView.visibility = View.GONE currentAnimator = null } }) start() } } }
Java
private void zoomImageFromThumb(final View thumbView, int imageResId) { // If there's an animation in progress, cancel it // immediately and proceed with this one. if (currentAnimator != null) { currentAnimator.cancel(); } // Load the high-resolution "zoomed-in" image. final ImageView expandedImageView = (ImageView) findViewById( R.id.expanded_image); expandedImageView.setImageResource(imageResId); // Calculate the starting and ending bounds for the zoomed-in image. // This step involves lots of math. Yay, math. final Rect startBounds = new Rect(); final Rect finalBounds = new Rect(); final Point globalOffset = new Point(); // The start bounds are the global visible rectangle of the thumbnail, // and the final bounds are the global visible rectangle of the container // view. Also set the container view's offset as the origin for the // bounds, since that's the origin for the positioning animation // properties (X, Y). thumbView.getGlobalVisibleRect(startBounds); findViewById(R.id.container) .getGlobalVisibleRect(finalBounds, globalOffset); startBounds.offset(-globalOffset.x, -globalOffset.y); finalBounds.offset(-globalOffset.x, -globalOffset.y); // Adjust the start bounds to be the same aspect ratio as the final // bounds using the "center crop" technique. This prevents undesirable // stretching during the animation. Also calculate the start scaling // factor (the end scaling factor is always 1.0). float startScale; if ((float) finalBounds.width() / finalBounds.height() > (float) startBounds.width() / startBounds.height()) { // Extend start bounds horizontally startScale = (float) startBounds.height() / finalBounds.height(); float startWidth = startScale * finalBounds.width(); float deltaWidth = (startWidth - startBounds.width()) / 2; startBounds.left -= deltaWidth; startBounds.right += deltaWidth; } else { // Extend start bounds vertically startScale = (float) startBounds.width() / finalBounds.width(); float startHeight = startScale * finalBounds.height(); float deltaHeight = (startHeight - startBounds.height()) / 2; startBounds.top -= deltaHeight; startBounds.bottom += deltaHeight; } // Hide the thumbnail and show the zoomed-in view. When the animation // begins, it will position the zoomed-in view in the place of the // thumbnail. thumbView.setAlpha(0f); expandedImageView.setVisibility(View.VISIBLE); // Set the pivot point for SCALE_X and SCALE_Y transformations // to the top-left corner of the zoomed-in view (the default // is the center of the view). expandedImageView.setPivotX(0f); expandedImageView.setPivotY(0f); // Construct and run the parallel animation of the four translation and // scale properties (X, Y, SCALE_X, and SCALE_Y). AnimatorSet set = new AnimatorSet(); set .play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left)) .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top)) .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f)) .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f)); set.setDuration(shortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { currentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { currentAnimator = null; } }); set.start(); currentAnimator = set; // Upon clicking the zoomed-in image, it should zoom back down // to the original bounds and show the thumbnail instead of // the expanded image. final float startScaleFinal = startScale; expandedImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (currentAnimator != null) { currentAnimator.cancel(); } // Animate the four positioning/sizing properties in parallel, // back to their original values. AnimatorSet set = new AnimatorSet(); set.play(ObjectAnimator .ofFloat(expandedImageView, View.X, startBounds.left)) .with(ObjectAnimator .ofFloat(expandedImageView, View.Y,startBounds.top)) .with(ObjectAnimator .ofFloat(expandedImageView, View.SCALE_X, startScaleFinal)) .with(ObjectAnimator .ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal)); set.setDuration(shortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { thumbView.setAlpha(1f); expandedImageView.setVisibility(View.GONE); currentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { thumbView.setAlpha(1f); expandedImageView.setVisibility(View.GONE); currentAnimator = null; } }); set.start(); currentAnimator = set; } }); }