【问题标题】:How to animate ImageView from center-crop to fill the screen and vice versa (facebook style)?如何从中心裁剪为 ImageView 设置动画以填充屏幕,反之亦然(facebook 风格)?
【发布时间】:2013-10-31 10:40:05
【问题描述】:

背景

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

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

使用我制作的下一个草图可以看到这一点:

问题

他们是怎么做到的?他们真的有 2 个视图动画来显示内容吗?

他们是如何让它变得像单一视图一样流畅?

我见过的唯一一个教程(链接here)在将缩略图设置为中心裁剪时无法很好地显示放大到全屏的图像。

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

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


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

我尝试过使用普通动画和其他动画技巧,但目前这对我来说是唯一有效的方法。

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

【问题讨论】:

  • 我做了一个自定义的 ImageView 和 ValueAnimator。当 valueAnimator 的值更新时,调用自定义 imageView 的 invalide()。然后,我用计算出的 Rect 调用 clipRect()。我认为它比更改 layoutParams 更有效。
  • @sso.techie 很有趣。您能否发布一个答案,甚至可以在 Github 上发布?

标签: android android-imageview android-animation crop


【解决方案1】:

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

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

正如我测试过的,还可以通过扩展 TouchImageView 类来添加缩放功能。您只需在开始时将缩放类型设置为 center-crop,然后在动画结束时将其设置回矩阵,如果需要,您可以将 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);
    }

}

【讨论】:

  • 非常好!谢谢!如果您仍然想知道为什么 AnimatorSet 不起作用,您仍然需要在调用 playTogether() 之后再调用 start()。见developer.android.com/reference/android/animation/…
  • set.start() 是必需的。此外,如果您对集合应用持续时间或插值,则必须在调用 set.playXX() 方法后调用适当的方法。
  • @M-WaJeEh 我需要先告诉它播放,然后再告诉它播放多长时间并使用哪种插值?怎么会这样?另外,顺便说一句,Android Lollipop 有一个很好的 API 用于此操作:developer.android.com/training/material/…
  • 是的,如果你看一下代码就会很清楚,而且set.playXX() 实际上并没有像我上面提到的那样播放。您必须致电set.start()set.playXX() 更像是在我调用start() 时播放这些动画。
【解决方案2】:

另一种解决方案,如果你只是想做一个从小到大的图片动画,你可以试试 ActivityOptions.makeThumbnailScaleUpAnimation 或者 makeScaleUpAnimation 看看是否适合你。

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

【讨论】:

  • 这是一件很棒的事情,但它也有一些缺点:只能放大到整个屏幕,只能在 API 16 及更高版本上使用,只能在活动之间工作,无法以任何方式自定义动画。
【解决方案3】:

你可以通过TransitionApi来实现这个,这个是resut gif:

下面的基本代码:

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

【讨论】:

  • 谢谢。是否没有针对较低 Android 版本的解决方案?也许使用支持库?
【解决方案4】:

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

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

【讨论】:

  • 确定你在这里所做的和我写的一样吗?我没有测试你写的东西,但是通过阅读代码,它似乎与它不匹配......另外我不确定你使用的变量是什么(比如 mImageFrame)。
  • 是的,这实现了非常相似的效果,无需使用动画师。它只使用 FragmentTransition,然后使用程序布局调整大小。上面的代码被简化了,例如您需要避免多次调用代码,显然您需要为 UI 视图设置成员字段。
  • 但是FB所拥有的并没有褪色。它是从中心裁剪(gridView 上的 imageView)到适合整个屏幕的移动+放大。我写的答案证明了这一点。我可以添加褪色,但这不是问题。褪色是更容易的部分。
  • 我同意我的代码看起来不如适当的动画好。但是,我想将我的详细视图分成不同的活动/片段(因为该页面具有不同的导航模式并显示其他 UI 功能),因此我希望在活动/片段转换中产生类似的影响。我想我会发布代码以防有人发现它有用。
  • 但这不是我问的,而且你没有解释你的代码如何让动画像我写的那样工作(而不仅仅是褪色)。
【解决方案5】:

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

你也可以看看这个帖子,他们做了类似的事情http://nerds.airbnb.com/host-experience-android/

【讨论】:

  • 这听起来与我提出的完全相同的解决方案(这里:stackoverflow.com/a/19616416/878126)......难道没有更好的方法,不会导致对周围的多个布局操作?也许覆盖 onDraw 并使用画布?
【解决方案6】:

我不确定为什么每个人都在谈论这个框架。有时使用其他人的代码可能很棒;但听起来你所追求的是对外观的精确控制。通过访问图形上下文,您可以获得它。在任何具有图形上下文的环境中,该任务都非常简单。在 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.
    

如果您的所有属性都由相同的参数驱动,则动画将是平滑的。作为额外的奖励,这里有一个计时技巧。只要您可以将 t 参数保持在 0 和 1 之间,就可以通过一行代码来破解缓入或缓出:

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

【讨论】:

  • 你能显示Android代码吗?我不明白这是如何工作的。
【解决方案7】:

我在Github做了一个示例代码。

这段代码的关键是使用canvas.clipRect()。 但是,它仅在 CroppedImageviewmatch_parent 时才有效。

简单来说, 我将比例和平移动画留给ViewPropertyAnimator。 然后,我可以专注于裁剪图像。

如上图,计算裁剪区域,并将裁剪区域更改为最终视图大小。

动画控制器

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

裁剪图像视图

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. 我没有实现特殊情况的代码...

【讨论】:

  • 这看起来效果很好。你也可以在这里显示代码吗?为什么它只有在“CroppedImageview is match_parent”时才有效?
猜你喜欢
  • 2017-07-08
  • 2014-06-14
  • 1970-01-01
  • 2013-08-20
  • 2015-07-08
  • 1970-01-01
  • 2022-07-07
  • 2011-03-02
  • 2016-03-29
相关资源
最近更新 更多