| Robert Ly | 3f53212 | 2012-10-03 14:20:26 -0700 | [diff] [blame] | 1 | page.title=Zooming a View |
| 2 | trainingnavtop=true |
| 3 | |
| 4 | @jd:body |
| 5 | |
| 6 | <div id="tb-wrapper"> |
| 7 | <div id="tb"> |
| 8 | <h2> |
| 9 | This lesson teaches you to: |
| 10 | </h2> |
| 11 | <ol> |
| 12 | <li> |
| 13 | <a href="#views">Create the Views</a> |
| 14 | </li> |
| 15 | <li> |
| 16 | <a href="#setup">Set up the Zoom Animation</a> |
| 17 | </li> |
| 18 | <li> |
| 19 | <a href="#animate">Zoom the View</a> |
| 20 | </li> |
| 21 | </ol> |
| Roman Nurik | fb80c9e | 2013-01-09 08:35:53 -0800 | [diff] [blame] | 22 | <h2> |
| 23 | Try it out |
| 24 | </h2> |
| 25 | <div class="download-box"> |
| 26 | <a href="{@docRoot}shareables/training/Animations.zip" class= |
| 27 | "button">Download the sample app</a> |
| 28 | <p class="filename"> |
| 29 | Animations.zip |
| 30 | </p> |
| 31 | </div> |
| Robert Ly | 3f53212 | 2012-10-03 14:20:26 -0700 | [diff] [blame] | 32 | </div> |
| 33 | </div> |
| 34 | <p> |
| 35 | This lesson demonstrates how to do a touch-to-zoom animation, which is useful for apps such as photo |
| 36 | galleries to animate a view from a thumbnail to a full-size image that fills the screen. |
| 37 | </p> |
| 38 | <p>Here's what a touch-to-zoom animation looks like that |
| 39 | expands an image thumbnail to fill the screen: |
| 40 | </p> |
| 41 | |
| 42 | <div class="framed-galaxynexus-land-span-8"> |
| 43 | <video class="play-on-hover" autoplay> |
| 44 | <source src="anim_zoom.mp4" type="video/mp4"> |
| 45 | <source src="anim_zoom.webm" type="video/webm"> |
| 46 | <source src="anim_zoom.ogv" type="video/ogg"> |
| 47 | </video> |
| 48 | </div> |
| 49 | <div class="figure-caption"> |
| 50 | Zoom animation |
| 51 | <div class="video-instructions"> </div> |
| 52 | </div> |
| 53 | |
| 54 | <p> |
| 55 | If you want to jump ahead and see a full working example, |
| 56 | <a href="{@docRoot}shareables/training/Animations.zip">download</a> and |
| 57 | run the sample app and select the |
| 58 | Zoom example. See the following files for the code implementation: |
| 59 | </p> |
| 60 | <ul> |
| 61 | <li> |
| 62 | <code>src/TouchHighlightImageButton.java</code> (a simple helper class that shows a blue |
| 63 | touch highlight when the image button is pressed) |
| 64 | </li> |
| 65 | <li> |
| 66 | <code>src/ZoomActivity.java</code> |
| 67 | </li> |
| 68 | <li> |
| 69 | <code>layout/activity_zoom.xml</code> |
| 70 | </li> |
| 71 | </ul> |
| 72 | <h2 id="views"> |
| 73 | Create the Views |
| 74 | </h2> |
| 75 | <p> |
| 76 | Create a layout file that contains the small and large version of the content that you want |
| 77 | to zoom. The following example creates an {@link android.widget.ImageButton} for clickable image thumbnail |
| 78 | and an {@link android.widget.ImageView} that displays the enlarged view of the image: |
| 79 | </p> |
| 80 | <pre> |
| 81 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| 82 | android:id="@+id/container" |
| 83 | android:layout_width="match_parent" |
| 84 | android:layout_height="match_parent"> |
| 85 | |
| 86 | <LinearLayout android:layout_width="match_parent" |
| 87 | android:layout_height="wrap_content" |
| 88 | android:orientation="vertical" |
| 89 | android:padding="16dp"> |
| 90 | |
| 91 | <ImageButton |
| 92 | android:id="@+id/thumb_button_1" |
| 93 | android:layout_width="100dp" |
| 94 | android:layout_height="75dp" |
| 95 | android:layout_marginRight="1dp" |
| 96 | android:src="@drawable/thumb1" |
| 97 | android:scaleType="centerCrop" |
| 98 | android:contentDescription="@string/description_image_1" /> |
| 99 | |
| 100 | </LinearLayout> |
| 101 | |
| 102 | <!-- This initially-hidden ImageView will hold the expanded/zoomed version of |
| 103 | the images above. Without transformations applied, it takes up the entire |
| 104 | screen. To achieve the "zoom" animation, this view's bounds are animated |
| 105 | from the bounds of the thumbnail button above, to its final laid-out |
| 106 | bounds. |
| 107 | --> |
| 108 | |
| 109 | <ImageView |
| 110 | android:id="@+id/expanded_image" |
| 111 | android:layout_width="match_parent" |
| 112 | android:layout_height="match_parent" |
| 113 | android:visibility="invisible" |
| 114 | android:contentDescription="@string/description_zoom_touch_close" /> |
| 115 | |
| 116 | </FrameLayout> |
| 117 | </pre> |
| 118 | <h2 id="setup"> |
| 119 | Set up the Zoom Animation |
| 120 | </h2> |
| 121 | <p> |
| 122 | Once you apply your layout, set up the event handlers that trigger the zoom animation. |
| 123 | The following example adds a {@link android.view.View.OnClickListener} to the {@link |
| 124 | android.widget.ImageButton} to execute the zoom animation when the user |
| 125 | clicks the image button: |
| 126 | </p> |
| 127 | <pre> |
| 128 | public class ZoomActivity extends FragmentActivity { |
| 129 | // Hold a reference to the current animator, |
| 130 | // so that it can be canceled mid-way. |
| 131 | private Animator mCurrentAnimator; |
| 132 | |
| 133 | // The system "short" animation time duration, in milliseconds. This |
| 134 | // duration is ideal for subtle animations or animations that occur |
| 135 | // very frequently. |
| 136 | private int mShortAnimationDuration; |
| 137 | |
| 138 | @Override |
| 139 | protected void onCreate(Bundle savedInstanceState) { |
| 140 | super.onCreate(savedInstanceState); |
| 141 | setContentView(R.layout.activity_zoom); |
| 142 | |
| 143 | // Hook up clicks on the thumbnail views. |
| 144 | |
| 145 | final View thumb1View = findViewById(R.id.thumb_button_1); |
| 146 | thumb1View.setOnClickListener(new View.OnClickListener() { |
| 147 | @Override |
| 148 | public void onClick(View view) { |
| 149 | zoomImageFromThumb(thumb1View, R.drawable.image1); |
| 150 | } |
| 151 | }); |
| 152 | |
| 153 | // Retrieve and cache the system's default "short" animation time. |
| 154 | mShortAnimationDuration = getResources().getInteger( |
| 155 | android.R.integer.config_shortAnimTime); |
| 156 | } |
| 157 | ... |
| 158 | } |
| 159 | </pre> |
| 160 | <h2 id="animate"> |
| 161 | Zoom the View |
| 162 | </h2> |
| 163 | <p> |
| 164 | You'll now need to animate from the normal sized view to the zoomed view |
| 165 | when appropriate. In general, you need to animate from the bounds of the normal-sized view to the |
| 166 | bounds of the larger-sized view. The following method shows you how to implement a zoom animation that |
| 167 | zooms from an image thumbnail to an enlarged view by doing the following things: |
| 168 | </p> |
| 169 | <ol> |
| 170 | <li>Assign the high-res image to the hidden "zoomed-in" (enlarged) {@link |
| 171 | android.widget.ImageView}. The following example loads a large image resource on the UI |
| 172 | thread for simplicity. You will want to do this loading in a separate thread to prevent |
| 173 | blocking on the UI thread and then set the bitmap on the UI thread. Ideally, the bitmap |
| 174 | should not be larger than the screen size. |
| 175 | </li> |
| 176 | <li>Calculate the starting and ending bounds for the {@link android.widget.ImageView}. |
| 177 | </li> |
| 178 | <li>Animate each of the four positioning and sizing properties <code>{@link |
| 179 | android.view.View#X}</code>, <code>{@link android.view.View#Y}</code>, ({@link |
| 180 | android.view.View#SCALE_X}, and <code>{@link android.view.View#SCALE_Y}</code>) |
| 181 | simultaneously, from the starting bounds to the ending bounds. These four animations are |
| 182 | added to an {@link android.animation.AnimatorSet} so that they can be started at the same |
| 183 | time. |
| 184 | </li> |
| 185 | <li>Zoom back out by running a similar animation but in reverse when the user touches the |
| 186 | screen when the image is zoomed in. You can do this by adding a {@link |
| 187 | android.view.View.OnClickListener} to the {@link android.widget.ImageView}. When clicked, the |
| 188 | {@link android.widget.ImageView} minimizes back down to the size of the image thumbnail and |
| 189 | sets its visibility to {@link android.view.View#GONE} to hide it. |
| 190 | </li> |
| 191 | </ol> |
| 192 | <pre> |
| 193 | private void zoomImageFromThumb(final View thumbView, int imageResId) { |
| 194 | // If there's an animation in progress, cancel it |
| 195 | // immediately and proceed with this one. |
| 196 | if (mCurrentAnimator != null) { |
| 197 | mCurrentAnimator.cancel(); |
| 198 | } |
| 199 | |
| 200 | // Load the high-resolution "zoomed-in" image. |
| 201 | final ImageView expandedImageView = (ImageView) findViewById( |
| 202 | R.id.expanded_image); |
| 203 | expandedImageView.setImageResource(imageResId); |
| 204 | |
| 205 | // Calculate the starting and ending bounds for the zoomed-in image. |
| 206 | // This step involves lots of math. Yay, math. |
| 207 | final Rect startBounds = new Rect(); |
| 208 | final Rect finalBounds = new Rect(); |
| 209 | final Point globalOffset = new Point(); |
| 210 | |
| 211 | // The start bounds are the global visible rectangle of the thumbnail, |
| 212 | // and the final bounds are the global visible rectangle of the container |
| 213 | // view. Also set the container view's offset as the origin for the |
| 214 | // bounds, since that's the origin for the positioning animation |
| 215 | // properties (X, Y). |
| 216 | thumbView.getGlobalVisibleRect(startBounds); |
| 217 | findViewById(R.id.container) |
| 218 | .getGlobalVisibleRect(finalBounds, globalOffset); |
| 219 | startBounds.offset(-globalOffset.x, -globalOffset.y); |
| 220 | finalBounds.offset(-globalOffset.x, -globalOffset.y); |
| 221 | |
| 222 | // Adjust the start bounds to be the same aspect ratio as the final |
| 223 | // bounds using the "center crop" technique. This prevents undesirable |
| 224 | // stretching during the animation. Also calculate the start scaling |
| 225 | // factor (the end scaling factor is always 1.0). |
| 226 | float startScale; |
| 227 | if ((float) finalBounds.width() / finalBounds.height() |
| 228 | > (float) startBounds.width() / startBounds.height()) { |
| 229 | // Extend start bounds horizontally |
| 230 | startScale = (float) startBounds.height() / finalBounds.height(); |
| 231 | float startWidth = startScale * finalBounds.width(); |
| 232 | float deltaWidth = (startWidth - startBounds.width()) / 2; |
| 233 | startBounds.left -= deltaWidth; |
| 234 | startBounds.right += deltaWidth; |
| 235 | } else { |
| 236 | // Extend start bounds vertically |
| 237 | startScale = (float) startBounds.width() / finalBounds.width(); |
| 238 | float startHeight = startScale * finalBounds.height(); |
| 239 | float deltaHeight = (startHeight - startBounds.height()) / 2; |
| 240 | startBounds.top -= deltaHeight; |
| 241 | startBounds.bottom += deltaHeight; |
| 242 | } |
| 243 | |
| 244 | // Hide the thumbnail and show the zoomed-in view. When the animation |
| 245 | // begins, it will position the zoomed-in view in the place of the |
| 246 | // thumbnail. |
| 247 | thumbView.setAlpha(0f); |
| 248 | expandedImageView.setVisibility(View.VISIBLE); |
| 249 | |
| 250 | // Set the pivot point for SCALE_X and SCALE_Y transformations |
| 251 | // to the top-left corner of the zoomed-in view (the default |
| 252 | // is the center of the view). |
| 253 | expandedImageView.setPivotX(0f); |
| 254 | expandedImageView.setPivotY(0f); |
| 255 | |
| 256 | // Construct and run the parallel animation of the four translation and |
| 257 | // scale properties (X, Y, SCALE_X, and SCALE_Y). |
| 258 | AnimatorSet set = new AnimatorSet(); |
| 259 | set |
| 260 | .play(ObjectAnimator.ofFloat(expandedImageView, View.X, |
| 261 | startBounds.left, finalBounds.left)) |
| 262 | .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, |
| 263 | startBounds.top, finalBounds.top)) |
| 264 | .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, |
| 265 | startScale, 1f)).with(ObjectAnimator.ofFloat(expandedImageView, |
| 266 | View.SCALE_Y, startScale, 1f)); |
| 267 | set.setDuration(mShortAnimationDuration); |
| 268 | set.setInterpolator(new DecelerateInterpolator()); |
| 269 | set.addListener(new AnimatorListenerAdapter() { |
| 270 | @Override |
| 271 | public void onAnimationEnd(Animator animation) { |
| 272 | mCurrentAnimator = null; |
| 273 | } |
| 274 | |
| 275 | @Override |
| 276 | public void onAnimationCancel(Animator animation) { |
| 277 | mCurrentAnimator = null; |
| 278 | } |
| 279 | }); |
| 280 | set.start(); |
| 281 | mCurrentAnimator = set; |
| 282 | |
| 283 | // Upon clicking the zoomed-in image, it should zoom back down |
| 284 | // to the original bounds and show the thumbnail instead of |
| 285 | // the expanded image. |
| 286 | final float startScaleFinal = startScale; |
| 287 | expandedImageView.setOnClickListener(new View.OnClickListener() { |
| 288 | @Override |
| 289 | public void onClick(View view) { |
| 290 | if (mCurrentAnimator != null) { |
| 291 | mCurrentAnimator.cancel(); |
| 292 | } |
| 293 | |
| 294 | // Animate the four positioning/sizing properties in parallel, |
| 295 | // back to their original values. |
| 296 | AnimatorSet set = new AnimatorSet(); |
| 297 | set.play(ObjectAnimator |
| 298 | .ofFloat(expandedImageView, View.X, startBounds.left)) |
| 299 | .with(ObjectAnimator |
| 300 | .ofFloat(expandedImageView, |
| 301 | View.Y,startBounds.top)) |
| 302 | .with(ObjectAnimator |
| 303 | .ofFloat(expandedImageView, |
| 304 | View.SCALE_X, startScaleFinal)) |
| 305 | .with(ObjectAnimator |
| 306 | .ofFloat(expandedImageView, |
| 307 | View.SCALE_Y, startScaleFinal)); |
| 308 | set.setDuration(mShortAnimationDuration); |
| 309 | set.setInterpolator(new DecelerateInterpolator()); |
| 310 | set.addListener(new AnimatorListenerAdapter() { |
| 311 | @Override |
| 312 | public void onAnimationEnd(Animator animation) { |
| 313 | thumbView.setAlpha(1f); |
| 314 | expandedImageView.setVisibility(View.GONE); |
| 315 | mCurrentAnimator = null; |
| 316 | } |
| 317 | |
| 318 | @Override |
| 319 | public void onAnimationCancel(Animator animation) { |
| 320 | thumbView.setAlpha(1f); |
| 321 | expandedImageView.setVisibility(View.GONE); |
| 322 | mCurrentAnimator = null; |
| 323 | } |
| 324 | }); |
| 325 | set.start(); |
| 326 | mCurrentAnimator = set; |
| 327 | } |
| 328 | }); |
| 329 | } |
| 330 | </pre> |