16

背景

我正在操作栏上实现“ 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);
}

这是动画本身的一部分,它为当前的 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();
}

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

我试过的

我已经使它与动态位图一起工作,但我不明白要对其进行更改以正确处理大小。

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

问题

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

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

同样,问题不在于获取位图或解码。这是关于如何让它们在这种效果下正常工作,而不会出现崩溃或奇怪的放大/缩小显示空白的情况。

4

1 回答 1

12

我查看了源代码,KenBurnsView实际上实现您想要的功能并不难,但我必须首先澄清一些事情:


1.动态加载图片

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

如果您知道自己在做什么,从 Internet 动态下载图像并不难,但是有很多方法可以做到。许多人实际上并没有提出自己的解决方案,而是使用像Volley这样的网络库来下载图像,或者他们直接使用Picasso或类似的东西。就我个人而言,我主要使用我自己的一组帮助类,但你必须决定你想如何下载图像。使用像Picasso这样的库很可能是您的最佳解决方案。我在这个答案中的代码示例将使用Picasso库,这是一个如何使用Picasso的快速示例:

Picasso.with(context).load("http://foo.com/bar.png").into(imageView);

2. 图像尺寸

...甚至不看输入图像的大小。

我真的不明白你的意思。在内部KenBurnsView用于ImageViews显示图像。他们负责正确缩放和显示图像,并且他们肯定会考虑图像的大小。我认为您的困惑可能是由scaleTypeImageViews. 如果您查看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>

请注意,有两个ImageViews而不是一个来创建交叉渐变效果。重要的部分是在两者上都可以找到的这个标签ImageViews

android:scaleType="centerCrop"

它的作用是告诉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- 例如在动画中 - 并且将ImageViews设置scaleType设置为其他内容,centerCrop那么您将失去视差效果!使用centerCropas scaleTypeof anImageView是一种创建类似视差效果的快速简便的方法。这个技巧的缺点可能是你注意到的:图中的图像ImageView很可能会被裁剪并且不完全可见!

如果您查看布局,您会发现其中的所有内容Views都有match_parentaslayout_heightlayout_width. 这对于某些图像作为match_parent约束也可能是一个问题,并且scaleTypes当图像远小于或大于ImageView.


translate 动画还考虑了图像的大小 - 或者至少是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);
}

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

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

  1. 而不是随机化开始值和结束值,您只需随机化开始值并根据开始值选择结束值。
  2. 您保留当前随机化所有值的策略,但您对每个值施加范围限制。例如fromScale应该是 和 之间的随机值和1.2f应该是 和 之间的随机值。1.4ftoScale1.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;
}

这里我修改了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);
}

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


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

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

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

本质上你只需要修改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");
            }
        });        
    }
}

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


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

您链接到的第二个库,这个,实际上包含一个更好的KenBurnsView. KenBurnsView我上面谈论的是一个子类,这种FrameLayout方法存在一些问题View。来自KenBurnsView第二个库的 是 的子类ImageView,这已经是一个巨大的改进。正因为如此,我们可以直接在上面使用像PicassoKenBurnsView这样的图像加载器库,而我们不必自己处理任何事情。您说您在使用第二个库时遇到随机崩溃?在过去的几个小时里,我一直在对其进行相当广泛的测试,并且没有遇到任何崩溃。

使用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" />

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

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

5. 测试

我在运行 Android 4.4.2 的 Nexus 5 上测试了所有内容。由于ViewPropertyAnimators使用了它,它应该在某个地方兼容到 API 级别 16,甚至可能是 12。

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

于 2014-05-22T15:58:29.800 回答