1 /*
2  * Copyright 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.animationsdemo;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.content.Intent;
24 import android.graphics.Point;
25 import android.graphics.Rect;
26 import android.os.Bundle;
27 import android.support.v4.app.FragmentActivity;
28 import android.support.v4.app.NavUtils;
29 import android.view.MenuItem;
30 import android.view.View;
31 import android.view.animation.DecelerateInterpolator;
32 import android.widget.ImageView;
33 
34 /**
35  * A sample showing how to zoom an image thumbnail to full-screen, by animating the bounds of the
36  * zoomed image from the thumbnail bounds to the screen bounds.
37  *
38  * <p>In this sample, the user can touch one of two images. Touching an image zooms it in, covering
39  * the entire activity content area. Touching the zoomed-in image hides it.</p>
40  */
41 public class ZoomActivity extends FragmentActivity {
42     /**
43      * Hold a reference to the current animator, so that it can be canceled mid-way.
44      */
45     private Animator mCurrentAnimator;
46 
47     /**
48      * The system "short" animation time duration, in milliseconds. This duration is ideal for
49      * subtle animations or animations that occur very frequently.
50      */
51     private int mShortAnimationDuration;
52 
53     @Override
onCreate(Bundle savedInstanceState)54     protected void onCreate(Bundle savedInstanceState) {
55         super.onCreate(savedInstanceState);
56         setContentView(R.layout.activity_zoom);
57 
58         // Hook up clicks on the thumbnail views.
59 
60         final View thumb1View = findViewById(R.id.thumb_button_1);
61         thumb1View.setOnClickListener(new View.OnClickListener() {
62             @Override
63             public void onClick(View view) {
64                 zoomImageFromThumb(thumb1View, R.drawable.image1);
65             }
66         });
67 
68         final View thumb2View = findViewById(R.id.thumb_button_2);
69         thumb2View.setOnClickListener(new View.OnClickListener() {
70             @Override
71             public void onClick(View view) {
72                 zoomImageFromThumb(thumb2View, R.drawable.image2);
73             }
74         });
75 
76         // Retrieve and cache the system's default "short" animation time.
77         mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
78     }
79 
80     @Override
onOptionsItemSelected(MenuItem item)81     public boolean onOptionsItemSelected(MenuItem item) {
82         switch (item.getItemId()) {
83             case android.R.id.home:
84                 // Navigate "up" the demo structure to the launchpad activity.
85                 // See http://developer.android.com/design/patterns/navigation.html for more.
86                 NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class));
87                 return true;
88         }
89 
90         return super.onOptionsItemSelected(item);
91     }
92 
93     /**
94      * "Zooms" in a thumbnail view by assigning the high resolution image to a hidden "zoomed-in"
95      * image view and animating its bounds to fit the entire activity content area. More
96      * specifically:
97      *
98      * <ol>
99      *   <li>Assign the high-res image to the hidden "zoomed-in" (expanded) image view.</li>
100      *   <li>Calculate the starting and ending bounds for the expanded view.</li>
101      *   <li>Animate each of four positioning/sizing properties (X, Y, SCALE_X, SCALE_Y)
102      *       simultaneously, from the starting bounds to the ending bounds.</li>
103      *   <li>Zoom back out by running the reverse animation on click.</li>
104      * </ol>
105      *
106      * @param thumbView  The thumbnail view to zoom in.
107      * @param imageResId The high-resolution version of the image represented by the thumbnail.
108      */
zoomImageFromThumb(final View thumbView, int imageResId)109     private void zoomImageFromThumb(final View thumbView, int imageResId) {
110         // If there's an animation in progress, cancel it immediately and proceed with this one.
111         if (mCurrentAnimator != null) {
112             mCurrentAnimator.cancel();
113         }
114 
115         // Load the high-resolution "zoomed-in" image.
116         final ImageView expandedImageView = (ImageView) findViewById(R.id.expanded_image);
117         expandedImageView.setImageResource(imageResId);
118 
119         // Calculate the starting and ending bounds for the zoomed-in image. This step
120         // involves lots of math. Yay, math.
121         final Rect startBounds = new Rect();
122         final Rect finalBounds = new Rect();
123         final Point globalOffset = new Point();
124 
125         // The start bounds are the global visible rectangle of the thumbnail, and the
126         // final bounds are the global visible rectangle of the container view. Also
127         // set the container view's offset as the origin for the bounds, since that's
128         // the origin for the positioning animation properties (X, Y).
129         thumbView.getGlobalVisibleRect(startBounds);
130         findViewById(R.id.container).getGlobalVisibleRect(finalBounds, globalOffset);
131         startBounds.offset(-globalOffset.x, -globalOffset.y);
132         finalBounds.offset(-globalOffset.x, -globalOffset.y);
133 
134         // Adjust the start bounds to be the same aspect ratio as the final bounds using the
135         // "center crop" technique. This prevents undesirable stretching during the animation.
136         // Also calculate the start scaling factor (the end scaling factor is always 1.0).
137         float startScale;
138         if ((float) finalBounds.width() / finalBounds.height()
139                 > (float) startBounds.width() / startBounds.height()) {
140             // Extend start bounds horizontally
141             startScale = (float) startBounds.height() / finalBounds.height();
142             float startWidth = startScale * finalBounds.width();
143             float deltaWidth = (startWidth - startBounds.width()) / 2;
144             startBounds.left -= deltaWidth;
145             startBounds.right += deltaWidth;
146         } else {
147             // Extend start bounds vertically
148             startScale = (float) startBounds.width() / finalBounds.width();
149             float startHeight = startScale * finalBounds.height();
150             float deltaHeight = (startHeight - startBounds.height()) / 2;
151             startBounds.top -= deltaHeight;
152             startBounds.bottom += deltaHeight;
153         }
154 
155         // Hide the thumbnail and show the zoomed-in view. When the animation begins,
156         // it will position the zoomed-in view in the place of the thumbnail.
157         thumbView.setAlpha(0f);
158         expandedImageView.setVisibility(View.VISIBLE);
159 
160         // Set the pivot point for SCALE_X and SCALE_Y transformations to the top-left corner of
161         // the zoomed-in view (the default is the center of the view).
162         expandedImageView.setPivotX(0f);
163         expandedImageView.setPivotY(0f);
164 
165         // Construct and run the parallel animation of the four translation and scale properties
166         // (X, Y, SCALE_X, and SCALE_Y).
167         AnimatorSet set = new AnimatorSet();
168         set
169                 .play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left,
170                         finalBounds.left))
171                 .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top,
172                         finalBounds.top))
173                 .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
174                 .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
175         set.setDuration(mShortAnimationDuration);
176         set.setInterpolator(new DecelerateInterpolator());
177         set.addListener(new AnimatorListenerAdapter() {
178             @Override
179             public void onAnimationEnd(Animator animation) {
180                 mCurrentAnimator = null;
181             }
182 
183             @Override
184             public void onAnimationCancel(Animator animation) {
185                 mCurrentAnimator = null;
186             }
187         });
188         set.start();
189         mCurrentAnimator = set;
190 
191         // Upon clicking the zoomed-in image, it should zoom back down to the original bounds
192         // and show the thumbnail instead of the expanded image.
193         final float startScaleFinal = startScale;
194         expandedImageView.setOnClickListener(new View.OnClickListener() {
195             @Override
196             public void onClick(View view) {
197                 if (mCurrentAnimator != null) {
198                     mCurrentAnimator.cancel();
199                 }
200 
201                 // Animate the four positioning/sizing properties in parallel, back to their
202                 // original values.
203                 AnimatorSet set = new AnimatorSet();
204                 set
205                         .play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left))
206                         .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
207                         .with(ObjectAnimator
208                                 .ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
209                         .with(ObjectAnimator
210                                 .ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
211                 set.setDuration(mShortAnimationDuration);
212                 set.setInterpolator(new DecelerateInterpolator());
213                 set.addListener(new AnimatorListenerAdapter() {
214                     @Override
215                     public void onAnimationEnd(Animator animation) {
216                         thumbView.setAlpha(1f);
217                         expandedImageView.setVisibility(View.GONE);
218                         mCurrentAnimator = null;
219                     }
220 
221                     @Override
222                     public void onAnimationCancel(Animator animation) {
223                         thumbView.setAlpha(1f);
224                         expandedImageView.setVisibility(View.GONE);
225                         mCurrentAnimator = null;
226                     }
227                 });
228                 set.start();
229                 mCurrentAnimator = set;
230             }
231         });
232     }
233 }
234