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



背景

我正在操作栏上实现">KenBurns效应"(演示此处),如此库的示例所示(移动的图标除外,我自己也这样做过)。

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

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

问题

当前的实现无法处理动态给定的多个位图(例如,来自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();
}

正如您所看到的,这并不考虑视图/位图的大小,只是随机选择如何缩放和平移。

我尝试过的

我已经让它与动态位图一起工作了,但我不知道该对它进行什么更改才能正确处理大小。

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

问题

为了正确地实现Ken Burns效果,使其能够处理动态创建的位图,应该做些什么?

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

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

我已经查看了KenBurnsView的源代码,实际上实现您想要的功能并不难,但我必须首先澄清以下几点:


1.动态加载图像

当前实现无法处理动态给定(例如,从互联网),。。。

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

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

2.图像大小

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

我真的不明白你的意思。在内部,KenBurnsView使用ImageViews来显示图像。他们负责正确缩放和显示图像,当然也会考虑图像的大小。我认为您的困惑可能是由为ImageViews设置的scaleType引起的。如果您查看包含KenBurnsView布局的布局文件R.layout.view_kenburns,您会看到:

<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"

我不记得这两者之间的确切区别,但它们都应该具有缩放图像的预期效果,使其既适合ImageView内部的X轴和Y轴,同时将其定中心在ImageView内部。

重要提示:更改scaleType可能会打乱KenBurnsView
如果您真的只是使用KenBurnsView来显示两个图像,那么除了图像的显示方式之外,更改scaleType也无关紧要,但如果您调整KenBurnsView的大小(例如在动画中),并且ImageViewsscaleType设置为centerCrop以外的其他值,则会失去视差效果!使用CCD_ 30作为CCD_ 32的CCD_。这个技巧的缺点可能是你注意到的: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);
}

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

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

  1. 不是随机化开始值和结束值,而是随机化开始值,并基于开始值拾取结束值
  2. 您保留了随机化所有值的当前策略,但对每个值都施加了范围限制。例如,fromScale应当是在1.2f1.4f之间的随机值,并且toScale应当是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;
}

在这里,我修改了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>,它包含了我们想要显示的图像的所有URL,urlIndex用于在urlList中循环。我把动画移到Callback中。这样,图像将在后台下载,一旦图像成功下载,动画将播放,ImageViews将交叉渐变。现在可以删除KenBurnsView中的许多旧代码,例如setResourceIds()fillImageViews()方法现在是不必要的。


4.解决方案#2:更好的KenBurnsView+Picasso

您链接到的第二个库,这个,实际上包含更好的KenBurnsView。我上面讨论的KenBurnsViewFrameLayout的一个子类,这个View所采用的方法存在一些问题。第二个库中的KenBurnsViewImageView的一个子类,这已经是一个巨大的改进。正因为如此,我们可以直接在KenBurnsView上使用Picasso之类的图像加载程序库,而且我们自己不必处理任何事情。您说您在第二个库中遇到随机崩溃?在过去的几个小时里,我对它进行了相当广泛的测试,没有遇到任何故障。

有了第二个库Picasso中的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()中,我们用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级。

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

最新更新