Background
I am working on an implementation of the "KenBurns effect" (demo here) on the action bar , as shown on this library's sample (except for the icon that moves, which I've done so myself).
In fact, I even asked about it a long time ago (here), which at this point I didn't even know its name. I was sure I've found a solution, but it has some problems.
Also, since I sometimes show the images from the device, some of them even need to be rotated, so I use a rotatableDrawable (as shown here).
The problem
The current implementation cannot handle multiple bitmaps that are given dynamically (from the Internet, for example), and doesn't even look at the input images' size.
Instead, it just does the zooming and translation in a random way, so many times it can zoom too much/little, and empty spaces can be shown.
The code
Here's the code that is related to the problems:
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);
}
And here's the part of the animation itself, which animates the current 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();
}
As you can see, this doesn't look at the view/bitmap sizes, and just randomly selects how to zoom and pan.
What I've tried
I've made it work with dynamic bitmaps, but I don't understand what to change on it so that it will handle the sizes correctly.
I've also noticed there is another library (here) that does this work, but it also has the same problems, and it's even harder to understand how to fix them there. Plus it randomly crashes . Here's a post I've reported about it.
The question
What should be done in order to implement Ken-Burns effect correctly, so that it could handle dynamically created bitmaps?
I'm thinking that maybe the best solution is to customize the way the ImageView draws its content, so that at any given time, it will show a part of the bitmap that is given to it, and the real animation would be between two rectangles of the bitmap . Sadly, I'm not sure how to do this.
Again, the question isn't about getting bitmaps or decoding. It's about how to make them work well with this effect without crashes or weird zoom in/out which show empty spaces.
I have look at the source code of the
KenBurnsView
and it isn't actually that hard to implement the features you want, but there are a few things I have to clarify first:1. Loading images dynamically
It isn't difficult to download images dynamically from the internet if you know what you are doing, but there are many ways to do it. Many people don't actually come up with their own solution but use a networking library like Volley to download the image or they go straight for Picasso or something similar. Personally I mostly use my own set of helper classes but you have to decide how exactly you want to download the images. Using a library like Picasso is most likely the best solution for you. My code samples in this answer will use the Picasso library, here is a quick example of how to use Picasso:
2. Image Size
I really don't understand what you mean by that. Internally the
KenBurnsView
usesImageViews
to display the images. They take care of properly scaling and displaying the image and they most certainly take the size of the images into account. I think your confusion might be caused by thescaleType
which is set for theImageViews
. If you look at the layout fileR.layout.view_kenburns
which contains the layout of theKenBurnsView
you see this:Notice that there are two
ImageViews
instead of just one to create the crossfade effect. The important part is this tag which is found on bothImageViews
:What this does is tell the
ImageView
to:ImageView
ImageView
ImageView
it will be cropped to the size of theImageView
So in its current state the images inside the
KenBurnsView
may be cropped at all times. If you want the image to scale to fit completely inside theImageView
so nothing has to be cropped or removed you need to change thescaleType
to one of those two:android:scaleType="fitCenter"
android:scaleType="centerInside"
I don't remember the exact difference between those two, but they should both have the desired effect of scaling the image so it fits both on the X and Y axis inside the
ImageView
while at the same time centering it inside theImageView
.IMPORTANT: Changing the
scaleType
potentially messes up theKenBurnsView
!If you really just use the
KenBurnsView
to display two images then changing thescaleType
won't matter aside from how the images are displayed, but if you resize theKenBurnsView
- for example in an Animation - and theImageViews
have thescaleType
set to something other thancenterCrop
you will loose the parallax effect! UsingcenterCrop
asscaleType
of anImageView
is a quick and easy way to create parallax-like effects. The drawback of this trick is probably what you noticed: The image in theImageView
will most likely be cropped and not completely visible!If you look at the layout you can see that all
Views
in there havematch_parent
aslayout_height
andlayout_width
. This could also be a problem for certain images as thematch_parent
constraint and certainscaleTypes
sometimes produce strange results when the images are considerably smaller or larger than theImageView
.The translate animation also takes the size of the image into account - or at least the size of the
ImageView
. If you look at the source code ofanimate(...)
andpickTranslation(...)
you will see this:So the view already accounts for the images size and how much the image is scaled when calculating the translation. So the concept of how this works is okay, the only problem I see is that both the start and end values are randomised without any dependencies between those two values. What this means is one simple thing: The start and endpoint of the animation might be the exact same position or may be very close to each other. As a result of that the animation may sometimes be very significant and other times barely noticeable at all.
I can think of three main ways to fix that:
fromScale
should be a random value between1.2f
and1.4f
andtoScale
should be a random value between1.6f
and1.8f
.Whether you choose approach #1 or #2 you are going to need this method:
Here I have modified the
animate()
method to force a certain distance between start and end points of the animation:As you can see I only need to modify how
fromScale
andtoScale
are calculated because the translations values are calculated from the scale values. This is not a 100% fix, but it is a big improvement.3. Solution #1: Fixing
KenBurnsView
(Use solution #2 if possible)
To fix the
KenBurnsView
you can implement the suggestions I mentioned above. Additionally we need to implement a way for the images to be added dynamically. The implementation of how theKenBurnsView
handles images is a little weird. We are going to need to modify that a bit. Since we are using Picasso this is actually going to be pretty simple:Essentially you just need to modify the
swapImage()
method, I tested it like this and it is working:I have omitted a few trivial parts,
urlList
is just aList<String>
which contains all the urls to the images we want to display,urlIndex
is used to cycle through theurlList
. I moved the animation into theCallback
. That way the image will be downloaded in the background and as soon as the image has been downloaded successfully the animations will play and theImageViews
will crossfade. A lot of the old code from theKenBurnsView
can now be deleted, for example the methodssetResourceIds()
orfillImageViews()
are now unnecessary.4. Solution #2: Better
KenBurnsView
+ PicassoThe second library you link to, this one, actually contains a MUCH better
KenBurnsView
. TheKenBurnsView
I talk about above is a subclass ofFrameLayout
and there are a few problems with the approach thisView
takes. TheKenBurnsView
from the second library is a subclass ofImageView
, this is already a huge improvement. Because of it we can use image loader libraries like Picasso directly on theKenBurnsView
and we don't have to take care of anything ourselves. You say that you experience random crashes with the second library? I have been testing it rather extensively the last few hours and didn't encounter a single crash.With the
KenBurnsView
from the second library and Picasso this all becomes very easy and very few lines of code, you just have to create aKenBurnsView
for example in xml:And then in your
Fragment
you first have to find the view in the layout and then inonViewCreated()
we load the image with Picasso:5. Testing
I tested everything on my Nexus 5 running Android 4.4.2. Since
ViewPropertyAnimators
are used this should all be compatible somewhere down to API Level 16, maybe even 12.I have a omitted a few lines of code here and there so if you have any questions feel free to ask!