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

时间:2014-05-15 13:42:47

标签: android animation android-animation image-manipulation android-kenburnsview

背景

我正在操作栏上执行“KenBurns effect”(演示here),如this library的示例所示(移动的图标除外)我自己就这样做了。)

事实上,我甚至在很久以前就问过它(here),此时我甚至都不知道它的名字。我确信我找到了解决方案,但它有一些问题。

此外,由于我有时会显示设备中的图像,因此有些图像甚至需要旋转,所以我使用了rotateDrawable(如图here所示)。

问题

当前实现无法处理动态提供的多个位图(例如,来自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)可以完成这项工作,但它也有同样的问题,而且更难理解如何修复它们。加上它随机崩溃。 Here's我发布了一篇关于它的帖子。

问题

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

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

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

1 个答案:

答案 0 :(得分:11)

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


1。动态加载图像

  

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

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

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

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>

请注意,有两个ImageViews而不是一个来创建交叉渐变效果。重要的部分是此标记,可在ImageViews

中找到
android:scaleType="centerCrop"

这样做是告诉ImageView

  1. 将图像置于ImageView
  2. 缩放图像,使其宽度适合ImageView
  3. 如果图片高于ImageView,则会将其裁剪为ImageView
  4. 的尺寸

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

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

    我不记得这两者之间的确切差异,但它们都应该具有缩放图像所需的效果,使其适合ImageView内的X和Y轴同时适用时间以ImageView

    为中心

    重要提示:更改scaleType可能会混淆KenBurnsView
    如果您真的只是使用KenBurnsView来显示两张图片,那么更改scaleType除了图像的显示方式外,还有其他问题,但如果您调整KenBurnsView的大小 - 例如在动画中 - ImageViewsscaleType设置为centerCrop以外的其他内容,您将失去视差效果!使用centerCrop作为scaleType的{​​{1}}是一种创建视差效果的快捷方法。这个技巧的缺点可能是您注意到的: ImageView中的图像很可能会被裁剪而不完全可见!

    如果您查看布局,可以看到其中的所有ImageView都有Views match_parentlayout_height。对于某些图像,这也可能是一个问题,因为layout_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);
    }
    

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

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

    1. 您可以随机选择开始值和结束值,而不是随机化 起始值并根据起始值选择结束值。
    2. 您保留当前策略以随机化所有值,但您对每个值施加范围限制。例如,fromScale应该是1.2f1.4f之间的随机值,toScale应该是1.6f1.8f之间的随机值。< / LI>
    3. 实施固定的翻译和缩放动画(换句话说,无聊的方式)。
    4. 无论您选择方法#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如何处理图像的实现有点奇怪。我们需要稍微修改一下。由于我们使用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");
                  }
              });        
          }
      }
      

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


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

      您链接到的第二个图书馆this one实际上包含了更多KenBurnsView。我在上面讨论的KenBurnsViewFrameLayout的子类,这个View采用的方法存在一些问题。来自second libraryKenBurnsViewImageView的子类,这已经是一个巨大的进步。因此,我们可以直接在KenBurnsView上使用Picasso等图像加载器库,而我们自己也不必处理任何事情。你说你遇到second library的随机崩溃?我在过去的几个小时里对它进行了相当广泛的测试,并没有遇到过一次崩溃。

      使用second libraryPicasso中的KenBurnsView,这一切都变得非常简单且代码很少,您只需要在xml中创建一个KenBurnsView

      <com.flaviofaria.kenburnsview.KenBurnsView
          android:id="@+id/kbvExample"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:src="@drawable/image" />
      

      然后在你的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);
      }
      

      5。测试

      我测试了运行Android 4.4.2的Nexus 5上的所有内容。由于使用了ViewPropertyAnimators,因此这应该在API级别16以下甚至12级都可以兼容。

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