可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Background
Facebook app has a nice transition animation between a small image on a post, and an enlarged mode of it that the user can also zoom to it.
As I see it, the animation not only enlarges and moves the imageView according to its previous location and size, but also reveals content instead of stretching the content of the imageView.
This can be seen using the next sketch i've made:
The question
How did they do it? did they really have 2 views animating to reveal the content?
How did they make it so fluid as if it's a single view?
the only tutorial i've seen (link here) of an image that is enlarged to full screen doesn't show well when the thumbnail is set to be center-crop.
Not only that, but it works even on low API of Android.
does anybody know of a library that has a similar ability?
EDIT: I've found a way and posted an answer, but it's based on changing the layoutParams , and i think it's not efficient and recommended.
I've tried using the normal animations and other animation tricks, but for now that's the only thing that worked for me.
If anyone know what to do in order to make it work in a better way, please write it down.
回答1:
Ok, i've found a possible way to do it.
i've made the layoutParams as variables that keep changing using the ObjectAnimator of the nineOldAndroids library. i think it's not the best way to achieve it since it causes a lot of onDraw and onLayout, but if the container has only a few views and doesn't change its size, maybe it's ok.
the assumption is that the imageView that i animate will take the exact needed size in the end, and that (currently) both the thumbnail and the animated imageView have the same container (but it should be easy to change it.
as i've tested, it is also possible to add zoom features by extending the TouchImageView class . you just set the scale type in the beginning to center-crop, and when the animation ends you set it back to matrix, and if you want, you can set the layoutParams to fill the entire container (and set the margin to 0,0).
i also wonder how come the AnimatorSet didn't work for me, so i will show here something that works, hoping someone could tell me what i should do.
here's the code:
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);
}
}
回答2:
Another solution, if you just want to make an animation of an image from small to large, you can try ActivityOptions.makeThumbnailScaleUpAnimation or makeScaleUpAnimationand see if they suit you.
http://developer.android.com/reference/android/app/ActivityOptions.html#makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int)
回答3:
You can achieve this through Transition
Api, and this the resut gif:
essential code below:
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);
}
View demo on github
sdk version >= 21
回答4:
I found a way to get a similar affect in a quick prototype. It might not be suitable for production use (I'm still investigating), but it is quick and easy.
Use a fade transition on your activity/fragment transition (which starts with the ImageView in exactly the same position). The fragment version:
final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
...etc
The activity version:
Intent intent = new Intent(context, MyDetailActivity.class);
startActivity(intent);
getActivity().overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
This gives a smooth transition without a flicker.
Adjust the layouts dynamically in the onStart() of the new fragment (you need to save member fields to the appropriate parts of your UI in onCreateView, and add some flags to ensure this code only gets called once).
@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://stackoverflow.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);
}
回答5:
I think the easiest way is to animate the height of the ImageView (a regular imageview, not necessary a custom view) while keeping the scaleType to centerCrop until full height, which you can know in advance if you set the image height to wrap_content in your layout and then use a ViewTreeObserver to know when the layout has ended, so you can get the ImageView height and then set the new "collapsed" height. I have not tested it but this is how I would do it.
You can also have a look at this post, they do something similar http://nerds.airbnb.com/host-experience-android/
回答6:
I'm not sure why everyone is talking about the framework. Using other peoples code can be great at times; but it sounds like what you are after is precise control over the look. By getting access to the graphics context you can have that. The task is pretty simple in any environment that has a graphics context. In android you can get it by overriding the onDraw
method and using the Canvas Object. It has everything you need to draw an image at many different scales, positions and clippings. You can even use a matrix if your familiar with that type of thing.
Steps
Make sure you have exact control of positioning, scale, and clip. This means disabling any layouts or auto-alignment that might be setup inside your objects container.
Figure you out what your parameter t
will be for linear interpolation and how you will want it to relate to time. How fast or slow, and will there be any easing. t
should be dependent on time.
After the thumbnails are cached, load the full scale image in the background. But don't show it yet.
When the animation trigger fires, show the large image and drive your animation with your t
parameter using interpolation between the initial properties' states to the final properties' states. Do this for all three properties, position, scale and clip. So for all properties do the following:
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.
If all your properties are driven by the same parameter the animation will be smooth. As an added bonus, here is a tip for timing. As long as you can keep your t
parameter between 0 and 1, easing in or out can be hacked with one line of code:
// After your t is all setup
t = t * t; // for easing in
// or
t = Math.sqrt(t); // for easing out
回答7:
I made a sample code in Github.
The key of this code is using canvas.clipRect()
.
But, it only works when the CroppedImageview
is match_parent
.
To explain simply,
I leave scale and translation animation to ViewPropertyAnimator
.
Then, I can focus on cropping the image.
Like above picture, calculate the clipping region, and change the clip region to final view size.
AnimationController
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()
}
it only works when the CroppedImageview is match_parent,
because
- The paths from start to end is included in CroppedImageView. If not, animation is not shown. So, Making it's size match_parent is easy to think.
- I didn't implement the code for special case...