使用动态位图设置在Android上实现KenBurns效果

and*_*per 16 animation android image-manipulation android-animation android-kenburnsview

背景

我正在动作栏上实现" KenBurns效果 "(演示在这里),如图书馆的样本所示(移动的图标除外,我自己就是这样做的).

事实上,我甚至在很久以前(这里)问过这个问题,在这一点上我甚至都不知道它的名字.我确信我找到了解决方案,但它有一些问题.

此外,因为我有时从设备显示图像,有的甚至需要旋转,所以我用一个rotatableDrawable(如图所示这里).

问题

当前实现无法处理动态给出的多个位图(例如,来自Internet),甚至不能查看输入图像的大小.

相反,它只是以随机方式进行缩放和平移,因此很多时候它可以缩放太多/很少,并且可以显示空白空间.

代码

这是与问题相关的代码:

private float pickScale() {
    return MIN_SCALE_FACTOR + this.random.nextFloat() * (MAX_SCALE_FACTOR - MIN_SCALE_FACTOR);
}

private float pickTranslation(final int value, final float ratio) {
    return value * (ratio - 1.0f) * (this.random.nextFloat() - 0.5f);
}

public void animate(final ImageView view) {
    final float fromScale = pickScale();
    final float toScale = pickScale();
    final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
    final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);
    final float toTranslationX = pickTranslation(view.getWidth(), toScale);
    final float toTranslationY = pickTranslation(view.getHeight(), toScale);
    start(view, KenBurnsView.DELAY_BETWEEN_IMAGE_SWAPPING_IN_MS, fromScale, toScale, fromTranslationX,
            fromTranslationY, toTranslationX, toTranslationY);
}
Run Code Online (Sandbox Code Playgroud)

这是动画本身的一部分,它动画当前的ImageView:

private void start(View view, long duration, float fromScale, float toScale, float fromTranslationX, float fromTranslationY, float toTranslationX, float toTranslationY) {
    view.setScaleX(fromScale);
    view.setScaleY(fromScale);
    view.setTranslationX(fromTranslationX);
    view.setTranslationY(fromTranslationY);
    ViewPropertyAnimator propertyAnimator = view.animate().translationX(toTranslationX).translationY(toTranslationY).scaleX(toScale).scaleY(toScale).setDuration(duration);
    propertyAnimator.start();
}
Run Code Online (Sandbox Code Playgroud)

如您所见,这不会查看视图/位图大小,只是随机选择如何缩放和平移.

我试过的

我已经使用动态位图,但我不明白要改变什么,以便它将正确处理大小.

我还注意到有另一个库(这里)可以完成这项工作,但它也有同样的问题,而且更难理解如何在那里修复它们.加上它随机崩溃.这是我报道的关于它的帖子.

这个问题

为了正确实现Ken-Burns效应应该怎么做才能处理动态创建的位图?

我想也许最好的解决方案是自定义ImageView绘制内容的方式,这样在任何给定的时间,它都会显示给它的位图的一部分,真正的动画将在两个矩形之间的位图.可悲的是,我不知道该怎么做.

同样,问题不在于获取位图或解码.这是关于如何使它们在没有崩溃或奇怪的放大/缩小显示空白空间的情况下使用此效果.

Xav*_*ler 11

我已经看过了它的源代码,KenBurnsView实际上并没有那么难实现你想要的功能,但我必须首先澄清一些事情:


1.动态加载图像

当前的实现无法处理动态给出的多个位图(例如,来自Internet),...

如果您知道自己在做什么,从互联网上动态下载图像并不困难,但有很多方法可以做到.许多人实际上并没有提出他们自己的解决方案,而是使用像Volley这样的网络库来下载图像,或者直接使用Picasso或类似的东西.就个人而言,我大多使用自己的帮助程序类,但你必须决定你想要下载图像的确切程度.使用毕加索这样的库很可能是最适合您的解决方案.我在这个答案中的代码示例将使用Picasso库,这是一个如何使用Picasso的简单示例:

Picasso.with(context).load("http://foo.com/bar.png").into(imageView);
Run Code Online (Sandbox Code Playgroud)

2.图像大小

...甚至没有看输入图像的大小.

我真的不明白你的意思.在内部,KenBurnsView用途ImageViews来显示图像.它们负责正确缩放和显示图像,并且它们肯定会考虑图像的大小.我认为你的困惑可能是由于scaleType为此而设置的ImageViews.如果你看一下R.layout.view_kenburns包含你的布局的布局文件,KenBurnsView你会看到:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image0"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ImageView
        android:id="@+id/image1"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>
Run Code Online (Sandbox Code Playgroud)

请注意,有两个ImageViews而不是一个来创建交叉淡化效果.重要的部分是这个标签,可以在以下两个标签上找到ImageViews:

android:scaleType="centerCrop"
Run Code Online (Sandbox Code Playgroud)

这样做是告诉ImageView:

  1. 将图像置于中心内 ImageView
  2. 缩放图像,使其宽度适合 ImageView
  3. 如果图像高于ImageView它将被裁剪为的大小ImageView

因此,在其当前状态下,KenBurnsView可以一直裁剪其中的图像.如果您希望图像缩放以完全适合内部,ImageView那么无需裁剪或删除任何内容,您需要将其更改scaleType为以下两者之一:

  • android:scaleType="fitCenter"
  • android:scaleType="centerInside"

我不记得这两者之间的确切差异,但是它们都应该具有缩放图像所需的效果,因此它同时适合在X和Y轴内ImageView同时将其居中ImageView.

重要提示:改变scaleType潜在的混乱KenBurnsView!
如果你真的只是用KenBurnsView来显示两个图像,则更改scaleType将不会从如何图像显示事不谈,但如果你调整KenBurnsView-例如在动画-和ImageViewsscaleType设定为比其他的东西centerCrop,你将失去的视差效应!使用centerCrop作为scaleTypeImageView是一个快速简便的方法来创建视差般的效果.这个技巧的缺点可能是你注意到的:遗嘱中的图像ImageView最有可能被裁剪而不是完全可见!

如果你看一下布局,你可以看到,所有Views在那里有match_parent作为layout_heightlayout_width.对于某些图像而言,这也可能是一个问题,因为当图像明显小于或大于match_parent约束时,约束scaleTypes有时会产生奇怪的结果ImageView.


翻译动画也考虑了图像的大小 - 或者至少是图像的大小ImageView.如果你看一下源代码animate(...)pickTranslation(...)你会看到:

// The value which is passed to pickTranslation() is the size of the View!
private float pickTranslation(final int value, final float ratio) {
    return value * (ratio - 1.0f) * (this.random.nextFloat() - 0.5f);
}

public void animate(final ImageView view) {
    final float fromScale = pickScale();
    final float toScale = pickScale();

    // Depending on the axis either the width or the height is passed to pickTranslation()
    final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
    final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);
    final float toTranslationX = pickTranslation(view.getWidth(), toScale);
    final float toTranslationY = pickTranslation(view.getHeight(), toScale);

    start(view, KenBurnsView.DELAY_BETWEEN_IMAGE_SWAPPING_IN_MS, fromScale, toScale, fromTranslationX, fromTranslationY, toTranslationX, toTranslationY);
}
Run Code Online (Sandbox Code Playgroud)

因此,视图已经考虑了图像大小以及在计算翻译时缩放图像的程度.所以这个工作原理的概念是可以的,我看到的唯一问题是开始值和结束值都是随机的,两个值之间没有任何依赖关系.这意味着一件简单的事情:动画的起点和终点可能是完全相同的位置,也可能彼此非常接近.因此,动画有时可能非常重要,而其他时候根本不会引起注意.

我可以想出三种主要方法来解决这个问题:

  1. 您可以随机化起始值并根据起始值选择结束值,而不是随机化开始值和结束值.
  2. 您保留当前策略以随机化所有值,但是您对每个值施加范围限制.例如,fromScale应该之间的随机值1.2f1.4ftoScale应之间的随机值1.6f1.8f.
  3. 实现固定的翻译和缩放动画(换句话说,无聊的方式).

无论您选择方法#1还是#2,您都需要这种方法:

// Returns a random value between min and max
private float randomRange(float min, float max) {
    return random.nextFloat() * (max - min) + max;
}
Run Code Online (Sandbox Code Playgroud)

在这里,我修改了animate()方法以强制动画的起点和终点之间的某个距离:

public void animate(View view) {
    final float fromScale = randomRange(1.2f, 1.4f);
    final float fromTranslationX = pickTranslation(view.getWidth(), fromScale);
    final float fromTranslationY = pickTranslation(view.getHeight(), fromScale);


    final float toScale =  randomRange(1.6f, 1.8f);
    final float toTranslationX = pickTranslation(view.getWidth(), toScale);
    final float toTranslationY = pickTranslation(view.getHeight(), toScale);

    start(view, this.mSwapMs, fromScale, toScale, fromTranslationX, fromTranslationY, toTranslationX, toTranslationY);
}
Run Code Online (Sandbox Code Playgroud)

如您所见,我只需要修改方式fromScaletoScale计算方法,因为翻译值是根据比例值计算的.这不是100%修复,但它是一个很大的改进.


3.解决方案#1:修复 KenBurnsView

(如果可能,使用解决方案#2)

要解决这个问题,KenBurnsView您可以实施我上面提到的建议.此外,我们需要实现一种动态添加图像的方法.KenBurnsView处理图像的实现方式有点奇怪.我们需要稍微修改一下.由于我们使用Picasso,这实际上非常简单:

基本上你只需要修改swapImage()方法,我像这样测试它并且它正在工作:

private void swapImage() {
    if (this.urlList.size() > 0) {
        if(mActiveImageIndex == -1) {
            mActiveImageIndex = 1;
            animate(mImageViews[mActiveImageIndex]);
            return;
        }

        final int inactiveIndex = mActiveImageIndex;
        mActiveImageIndex = (1 + mActiveImageIndex) % mImageViews.length;
        Log.d(TAG, "new active=" + mActiveImageIndex);

        String url = this.urlList.get(this.urlIndex++);
        this.urlIndex = this.urlIndex % this.urlList.size();

        final ImageView activeImageView = mImageViews[mActiveImageIndex];
        activeImageView.setAlpha(0.0f);

        Picasso.with(this.context).load(url).into(activeImageView, new Callback() {

            @Override
            public void onSuccess() {
                ImageView inactiveImageView = mImageViews[inactiveIndex];

                animate(activeImageView);

                AnimatorSet animatorSet = new AnimatorSet();
                animatorSet.setDuration(mFadeInOutMs);
                animatorSet.playTogether(
                        ObjectAnimator.ofFloat(inactiveImageView, "alpha", 1.0f, 0.0f),
                        ObjectAnimator.ofFloat(activeImageView, "alpha", 0.0f, 1.0f)
                );
                animatorSet.start();
            }

            @Override
            public void onError() {
                Log.i(LOG_TAG, "Could not download next image");
            }
        });        
    }
}
Run Code Online (Sandbox Code Playgroud)

我省略了一些琐碎的部分,urlList只是一个List<String>包含我们想要显示的图像的所有网址,urlIndex用于循环通过urlList.我把动画移到了Callback.这样,图像将在后台下载,并且一旦成功下载图像,动画将播放并且ImageViews将交叉淡入淡出.KenBurnsView现在可以删除许多旧代码,例如方法setResourceIds()fillImageViews()现在不需要.


4.解决方案#2:更好KenBurnsView+毕加索

您链接到的第二个库,这个库实际上包含了更多KenBurnsView.在KenBurnsView我讲上面的一个子类FrameLayout,并有与此方法有几个问题View需要.在KenBurnsView第二个库是的子类ImageView,这已经是一个巨大的进步.因此我们可以直接在Picasso上使用图像加载器库KenBurnsView,我们不必自己处理任何事情.你说你遇到第二个库的随机崩溃?在过去的几个小时里,我一直在广泛地测试它,并没有遇到任何一次崩溃.

随着KenBurnsView第二库毕加索这一切都变得非常容易,代码非常几行,你只需要创建一个KenBurnsView例如XML:

<com.flaviofaria.kenburnsview.KenBurnsView
    android:id="@+id/kbvExample"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/image" />
Run Code Online (Sandbox Code Playgroud)

然后在Fragment你的首先必须在布局中找到视图然后在onViewCreated()我们用Picasso加载图像:

private KenBurnsView kbvExample;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_kenburns_test, container, false);

    this.kbvExample = (KenBurnsView) view.findViewById(R.id.kbvExample);

    return view;
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    Picasso.with(getActivity()).load(IMAGE_URL).into(this.kbvExample);
}
Run Code Online (Sandbox Code Playgroud)

5.测试

我测试了运行Android 4.4.2的Nexus 5上的所有内容.自从ViewPropertyAnimators使用以来,这应该在API级别16以下可能兼容,甚至可能是12.

我在这里和那里省略了几行代码,所以如果你有任何问题可以随意提问!