如何从中心裁剪动画ImageView填充屏幕,反之亦然(脸谱风格)?

时间:2013-10-22 13:54:16

标签: android android-imageview android-animation crop

背景

Facebook应用在帖子上的小图片和用户也可以缩放的放大模式之间有一个很好的过渡动画。

在我看来,动画不仅根据以前的位置和大小放大和移动imageView,还会显示内容而不是拉伸imageView的内容。

使用下一张草图可以看出:

enter image description here

问题

他们是怎么做到的?他们真的有2个观点动画来揭示内容吗?

他们是如何让它变得如此流畅,好像它是一个单一的视图?

当缩略图设置为中心裁剪时,我已经看到的唯一一个放大到全屏的图像(链接here)并不能很好地显示。

不仅如此,它甚至可以在Android的低API上运行。

有人知道有类似能力的图书馆吗?


编辑:我找到了一种方法和posted an answer,但它基于更改layoutParams,我认为它没有效率和推荐。

我尝试过使用普通的动画和其他动画技巧,但是现在这是唯一对我有用的东西。

如果有人知道该怎么做才能让它以更好的方式运作,请写下来。

7 个答案:

答案 0 :(得分:4)

好的,我找到了一种可行的方法。 我已将layoutParams作为变量使用nineOldAndroids library的ObjectAnimator进行更改。我认为这不是实现它的最佳方式,因为它会导致很多onDraw和onLayout,但如果容器只有少量视图而且没有改变它的大小,也许没关系。

假设我动画的imageView最终将采用确切的所需大小,并且(当前)缩略图和动画imageView都具有相同的容器(但应该很容易更改它。

正如我测试的那样,也可以通过扩展TouchImageView类来添加缩放功能。你只需在开始到中心裁剪时设置比例类型,当动画结束时你将它设置回矩阵,如果你愿意,你可以设置layoutParams来填充整个容器(并将边距设置为0,0) )。

我也想知道AnimatorSet怎么不适合我,所以我会在这里展示一些有用的东西,希望有人能告诉我应该做些什么。

这是代码:

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);
    }

}

答案 1 :(得分:2)

另一种解决方案,如果您只想制作从小到大的图像动画,可以尝试使用ActivityOptions.makeThumbnailScaleUpAnimation或makeScaleUpAnimation,看看它们是否适合您。

http://developer.android.com/reference/android/app/ActivityOptions.html#makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int)

答案 2 :(得分:1)

你可以通过Transition Api实现这一点,这就是resut gif:

Demo

以下基本代码:

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版本&gt; = 21

答案 3 :(得分:0)

我找到了一种在快速原型中获得类似效果的方法。它可能不适合生产使用(我还在调查),但它很快捷。

  1. 对活动/片段转换使用淡入淡出转换(以完全相同的位置从ImageView开始)。片段版本:

    final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();    
    fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);    
    ...etc    
    

    活动版本:

    Intent intent = new Intent(context, MyDetailActivity.class);
    startActivity(intent);
    getActivity().overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
    

    这样可以平滑过渡而不会出现闪烁。

  2. 在新片段的onStart()中动态调整布局(您需要在onCreateView中将成员字段保存到UI的相应部分,并添加一些标志以确保此代码仅被调用一次)。

    @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);
    }    
    

答案 4 :(得分:0)

我认为最简单的方法是设置ImageView的高度(常规图像视图,不一定是自定义视图),同时将scaleType保持到centerCrop直到全高,如果将图像高度设置为在布局中使用wrap_content,然后使用ViewTreeObserver知道布局何时结束,这样您就可以获得ImageView高度,然后设置新的“折叠”高度。我没有测试过,但这就是我要做的。

你也可以查看这篇文章,他们做了类似的事情http://nerds.airbnb.com/host-experience-android/

答案 5 :(得分:0)

我不确定为什么每个人都在谈论这个框架。使用其他人的代码有时可能很棒;但听起来你所追求的是精确控制外观。通过访问图形上下文,您可以拥有它。在具有图形上下文的任何环境中,任务都非常简单。在android中,您可以通过覆盖onDraw方法并使用Canvas对象来获取它。它拥有您在许多不同比例,位置和剪报中绘制图像所需的一切。如果您熟悉那种类型的东西,甚至可以使用矩阵。

<强>步骤

  1. 确保您可以精确控制定位,缩放和剪辑。这意味着禁用可能在对象容器内设置的任何布局或自动对齐。

  2. 找出你的参数t对线性插值的影响以及你希望它与时间的关系。多快或多慢,并且会有任何宽松。 t应该依赖于时间。

  3. 缓存缩略图后,在后台加载完整比例图像。但是还没有显示出来。

  4. 当动画触发器触发时,显示大图像并使用t参数使用初始属性状态到最终属性状态之间的插值来驱动动画。对所有三个属性,位置,比例和剪辑执行此操作。因此,对于所有属性,请执行以下操作:

    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.
    
  5. 如果所有属性都由相同的参数驱动,则动画将是平滑的。作为额外的奖励,这里是时间的提示。只要您可以将t参数保持在0和1之间,就可以使用一行代码破解缓入或退出:

    // After your t is all setup
    t = t * t;        // for easing in
    // or
    t = Math.sqrt(t); // for easing out
    

答案 6 :(得分:0)

我在Github中创建了示例代码。

此代码的键是使用canvas.clipRect()。 但是,仅当CroppedImageviewmatch_parent时有效。

为了简单说明, 我将缩放和平移动画留给ViewPropertyAnimator。 然后,我可以专注于裁剪图像。

clipping descriptions

像上面的图片一样,计算裁剪区域,然后将裁剪区域更改为最终视图大小。

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()
}

仅在CroppedImageview为match_parent时有效, 因为

  1. CroppedImageView中包含从开始到结束的路径。如果不是,则不显示动画。因此,将其设置为match_parent的大小很容易想到。
  2. 我没有实现特殊情况的代码...