源码
1 activity_zoom.xml
2 strings.xml
ZoomAnimation Hello world! Settings Touch a photo to expand it. Image 1 Image 2 Expanded image (touch to close)
3 TouchHighlightImageButton.java
package com.example.zoomanimation; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.widget.ImageButton; public class TouchHighlightImageButton extends ImageButton { private Drawable mForegroundDrawable; private Rect mCachedBounds = new Rect(); public TouchHighlightImageButton(Context context) { super(context); init(); } public TouchHighlightImageButton(Context context, AttributeSet attrs) { super(context, attrs); init(); } public TouchHighlightImageButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { // Reset default ImageButton background and padding. setBackgroundColor(0); setPadding(0, 0, 0, 0); // Retrieve the drawable resource assigned to the // android.R.attr.selectableItemBackground // theme attribute from the current theme. TypedArray a = getContext().obtainStyledAttributes(new int[] { android.R.attr.selectableItemBackground }); mForegroundDrawable = a.getDrawable(0); mForegroundDrawable.setCallback(this); a.recycle(); } @Override protected void drawableStateChanged() { super.drawableStateChanged(); // Update the state of the highlight drawable to match // the state of the button. if (mForegroundDrawable.isStateful()) { mForegroundDrawable.setState(getDrawableState()); } // Trigger a redraw. invalidate(); } @Override protected void onDraw(Canvas canvas) { // First draw the image. super.onDraw(canvas); // Then draw the highlight on top of it. If the button is neither // focused // nor pressed, the drawable will be transparent, so just the image // will be drawn. mForegroundDrawable.setBounds(mCachedBounds); mForegroundDrawable.draw(canvas); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Cache the view bounds. mCachedBounds.set(0, 0, w, h); } }
4 ZoomActivity.java
package com.example.zoomanimation; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.app.Activity; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; public class ZoomActivity extends Activity { /** * Hold a reference to the current animator, so that it can be canceled * mid-way. */ private Animator mCurrentAnimator; /** * The system "short" animation time duration, in milliseconds. This * duration is ideal for subtle animations or animations that occur very * frequently. */ private int mShortAnimationDuration; @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); } }); final View thumb2View = findViewById(R.id.thumb_button_2); thumb2View.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { zoomImageFromThumb(thumb2View, R.drawable.image2); } }); // Retrieve and cache the system's default "short" animation time. mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); } /** * "Zooms" in a thumbnail view by assigning the high resolution image to a * hidden "zoomed-in" image view and animating its bounds to fit the entire * activity content area. More specifically: * **
* * @param thumbView * The thumbnail view to zoom in. * @param imageResId * The high-resolution version of the image represented by the * thumbnail. */ private void zoomImageFromThumb(final View thumbView, int imageResId) { // If there's an animation in progress, cancel it immediately and // proceed with this one. if (mCurrentAnimator != null) { mCurrentAnimator.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(mShortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCurrentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { mCurrentAnimator = null; } }); set.start(); mCurrentAnimator = 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 (mCurrentAnimator != null) { mCurrentAnimator.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(mShortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { thumbView.setAlpha(1f); expandedImageView.setVisibility(View.GONE); mCurrentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { thumbView.setAlpha(1f); expandedImageView.setVisibility(View.GONE); mCurrentAnimator = null; } }); set.start(); mCurrentAnimator = set; } }); } }- Assign the high-res image to the hidden "zoomed-in" (expanded) image * view.
*- Calculate the starting and ending bounds for the expanded view.
*- Animate each of four positioning/sizing properties (X, Y, SCALE_X, * SCALE_Y) simultaneously, from the starting bounds to the ending bounds.
*- Zoom back out by running the reverse animation on click.
*
分析
对zoomImageFromThumb方法添加Log,打印数据理解下:
private void zoomImageFromThumb(final View thumbView, int imageResId) { // If there's an animation in progress, cancel it immediately and // proceed with this one. if (mCurrentAnimator != null) { mCurrentAnimator.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); Log.d("ZoomActivity", "before startBounds =" + startBounds); Log.d("ZoomActivity", "before finalBounds =" + finalBounds); int[] thumbViewLocation = new int[2]; thumbView.getLocationOnScreen(thumbViewLocation); int thumbViewX = thumbViewLocation[0]; int thumbViewY = thumbViewLocation[1]; int[] containerLocation = new int[2]; findViewById(R.id.container).getLocationOnScreen(containerLocation); int containerX = containerLocation[0]; int containerY = containerLocation[1]; Log.d("ZoomActivity", "getLocationOnScreen thumbViewX =" + thumbViewX + " thumbViewY=" + thumbViewY); Log.d("ZoomActivity", "getLocationOnScreen containerX =" + containerX + " containerY=" + containerY); startBounds.offset(-globalOffset.x, -globalOffset.y); finalBounds.offset(-globalOffset.x, -globalOffset.y); Log.d("ZoomActivity", "after startBounds =" + startBounds); Log.d("ZoomActivity", "after finalBounds =" + finalBounds); // 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(mShortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCurrentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { mCurrentAnimator = null; } }); set.start(); mCurrentAnimator = 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 (mCurrentAnimator != null) { mCurrentAnimator.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(mShortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { thumbView.setAlpha(1f); expandedImageView.setVisibility(View.GONE); mCurrentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { thumbView.setAlpha(1f); expandedImageView.setVisibility(View.GONE); mCurrentAnimator = null; } }); set.start(); mCurrentAnimator = set; } }); }
Log:
01-05 00:05:33.585 16826 16826 D ZoomActivity: before startBounds =Rect(32, 246 - 232, 396)
01-05 00:05:33.585 16826 16826 D ZoomActivity: before finalBounds =Rect(0, 144 - 720, 1184)
01-05 00:05:33.586 16826 16826 D ZoomActivity: getLocationOnScreen thumbViewX =32 thumbViewY=246
01-05 00:05:33.586 16826 16826 D ZoomActivity: getLocationOnScreen containerX =0 containerY=144
01-05 00:05:33.586 16826 16826 D ZoomActivity: after startBounds =Rect(32, 102 - 232, 252)
01-05 00:05:33.586 16826 16826 D ZoomActivity: after finalBounds =Rect(0, 0 - 720, 1040)
A,
从Log知:getGlobalVisibleRect方法返回的Rect对象的左上角坐标与getLocationOnScreen方法是一致的,也就理解了:
getGlobalVisibleRect获取全局坐标系的一个视图区域, 返回一个填充的Rect对象,该Rect是基于总整个屏幕的。
对view进行offset处理是为了去除相对屏幕的偏移量,使”子View“的Rect的左上角坐标是相对"父View",这样利于计算”子View“相对”父View“的最终显示位置
B,
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;
}
为了使用相同的宽高比例,避免出现较大的不正常拉伸 ,需要进行计算:
1.满足if判断说明:“屏幕宽高比例” 大于 “缩略图宽高比例”,说明最终显示的“宽度”较大,为了较少变形拉伸,因此需要在“宽度”上进行调整,此时就应该拿相对比较接近的"原始高度"因素进行计算(为了较少偏差),首先通过"高度"计算出“缩放因子”(startScale),使用同样的缩放因子(startScale)计算出“应该有的原始宽度”,然后用“应该有的原始宽度”减去“真实的原始宽度“就是总的宽度偏差,然后除以2算出单侧的偏差(deltaHeight),然后对”真实的原始宽度“,用”单侧的偏差(deltaHeight)“进行微调
2.满足else判断说明:“屏幕宽高比例” 小于 “缩略图宽高比例”,说明最终显示的“高度”较大,为了较少变形拉伸,因此需要在“高度”上进行调整,类似上面分析...
上面使用了:”相同的宽高比例“及"中心剪切技术"等概念。