× {{alert.msg}} Never ask again
Get notified about new tutorials RECEIVE NEW TUTORIALS

Implementation of KenBurns effect on Android with dynamic bitmaps setting

Xaver Kapeller
Mar 13, 2015
<p>I have look at the source code of the <code>KenBurnsView</code> and it isn't actually that hard to implement the features you want, but there are a few things I have to clarify first:</p> <hr> <h1><strong>1. Loading images dynamically</strong></h1> <blockquote> <p>The current implementation cannot handle multiple bitmaps that are given dynamically (from the Internet, for example),...</p> </blockquote> <p>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 <a href="https://github.com/mcxiaoke/android-volley" rel="nofollow"><strong>Volley</strong></a> to download the image or they go straight for <a href="http://square.github.io/picasso/" rel="nofollow"><strong>Picasso</strong></a> 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 <a href="http://square.github.io/picasso/" rel="nofollow"><strong>Picasso</strong></a> is most likely the best solution for you. My code samples in this answer will use the <a href="http://square.github.io/picasso/" rel="nofollow"><strong>Picasso</strong></a> library, here is a quick example of how to use <a href="http://square.github.io/picasso/" rel="nofollow"><strong>Picasso</strong></a>:</p> <pre><code>Picasso.with(context).load("http://foo.com/bar.png").into(imageView); </code></pre> <hr> <h1><strong>2. Image Size</strong></h1> <blockquote> <p>...and doesn't even look at the input images' size.</p> </blockquote> <p>I really don't understand what you mean by that. Internally the <code>KenBurnsView</code> uses <code>ImageViews</code> 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 the <code>scaleType</code> which is set for the <code>ImageViews</code>. If you look at the layout file <code>R.layout.view_kenburns</code> which contains the layout of the <code>KenBurnsView</code> you see this:</p> <pre><code>&lt;FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"&gt; &lt;ImageView android:id="@+id/image0" android:scaleType="centerCrop" android:layout_width="match_parent" android:layout_height="match_parent" /&gt; &lt;ImageView android:id="@+id/image1" android:scaleType="centerCrop" android:layout_width="match_parent" android:layout_height="match_parent" /&gt; &lt;/FrameLayout&gt; </code></pre> <p>Notice that there are two <code>ImageViews</code> instead of just one to create the crossfade effect. The important part is this tag which is found on both <code>ImageViews</code>:</p> <pre><code>android:scaleType="centerCrop" </code></pre> <p>What this does is tell the <code>ImageView</code> to:</p> <ol> <li>Center the image inside the <code>ImageView</code></li> <li>Scale the image so its width fits inside the <code>ImageView</code></li> <li>If the image is taller than the <code>ImageView</code> it will be cropped to the size of the <code>ImageView</code></li> </ol> <p>So in its current state the images inside the <code>KenBurnsView</code> may be cropped at all times. If you want the image to scale to fit completely inside the <code>ImageView</code> so nothing has to be cropped or removed you need to change the <code>scaleType</code> to one of those two:</p> <ul> <li><code>android:scaleType="fitCenter"</code></li> <li><code>android:scaleType="centerInside"</code></li> </ul> <p>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 <code>ImageView</code> while at the same time centering it inside the <code>ImageView</code>.</p> <p><strong>IMPORTANT: Changing the <code>scaleType</code> potentially messes up the <code>KenBurnsView</code>!</strong><br> If you really just use the <code>KenBurnsView</code> to display two images then changing the <code>scaleType</code> won't matter aside from how the images are displayed, but if you resize the <code>KenBurnsView</code> - for example in an Animation - and the <code>ImageViews</code> have the <code>scaleType</code> set to something other than <code>centerCrop</code> you will loose the parallax effect! Using <code>centerCrop</code> as <code>scaleType</code> of an <code>ImageView</code> is a quick and easy way to create parallax-like effects. The drawback of this trick is probably what you noticed: <strong>The image in the <code>ImageView</code> will most likely be cropped and not completely visible!</strong> </p> <p>If you look at the layout you can see that all <code>Views</code> in there have <code>match_parent</code> as <code>layout_height</code> and <code>layout_width</code>. This could also be a problem for certain images as the <code>match_parent</code> constraint and certain <code>scaleTypes</code> sometimes produce strange results when the images are considerably smaller or larger than the <code>ImageView</code>.</p> <hr> <p>The translate animation also takes the size of the image into account - or at least the size of the <code>ImageView</code>. If you look at the source code of <code>animate(...)</code> and <code>pickTranslation(...)</code> you will see this:</p> <pre><code>// 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); } </code></pre> <p>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: <strong>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.</strong></p> <p>I can think of three main ways to fix that: </p> <ol> <li>Instead of randomising both start and end values you just randomise the start values and pick the end values based on the start values.</li> <li>You keep the current strategy of randomising all values, but you impose range restrictions on each value. For example the <code>fromScale</code> should be a random value between <code>1.2f</code> and <code>1.4f</code> and <code>toScale</code> should be a random value between <code>1.6f</code> and <code>1.8f</code>.</li> <li>Implement a fixed translation and scale animation (In other words the boring way). </li> </ol> <p>Whether you choose approach #1 or #2 you are going to need this method:</p> <pre><code>// Returns a random value between min and max private float randomRange(float min, float max) { return random.nextFloat() * (max - min) + max; } </code></pre> <p>Here I have modified the <code>animate()</code> method to force a certain distance between start and end points of the animation:</p> <pre><code>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); } </code></pre> <p>As you can see I only need to modify how <code>fromScale</code> and <code>toScale</code> are calculated because the translations values are calculated from the scale values. This is not a 100% fix, but it is a big improvement.</p> <hr> <h1><strong>3. Solution #1: Fixing <code>KenBurnsView</code></strong></h1> <h2><strong>(Use solution #2 if possible)</strong></h2> <p>To fix the <code>KenBurnsView</code> 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 the <code>KenBurnsView</code> handles images is a little weird. We are going to need to modify that a bit. Since we are using <a href="http://square.github.io/picasso/" rel="nofollow"><strong>Picasso</strong></a> this is actually going to be pretty simple:</p> <p>Essentially you just need to modify the <code>swapImage()</code> method, I tested it like this and it is working:</p> <pre><code>private void swapImage() { if (this.urlList.size() &gt; 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"); } }); } } </code></pre> <p>I have omitted a few trivial parts, <code>urlList</code> is just a <code>List&lt;String&gt;</code> which contains all the urls to the images we want to display, <code>urlIndex</code> is used to cycle through the <code>urlList</code>. I moved the animation into the <code>Callback</code>. 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 the <code>ImageViews</code> will crossfade. A lot of the old code from the <code>KenBurnsView</code> can now be deleted, for example the methods <code>setResourceIds()</code> or <code>fillImageViews()</code> are now unnecessary.</p> <hr> <h1><strong>4. Solution #2: Better <code>KenBurnsView</code> + Picasso</strong></h1> <p>The second library you link to, <a href="https://github.com/flavioarfaria/KenBurnsView" rel="nofollow"><strong>this one</strong></a>, actually contains a MUCH better <code>KenBurnsView</code>. The <code>KenBurnsView</code> I talk about above is a subclass of <code>FrameLayout</code> and there are a few problems with the approach this <code>View</code> takes. The <code>KenBurnsView</code> from the <a href="https://github.com/flavioarfaria/KenBurnsView" rel="nofollow"><strong>second library</strong></a> is a subclass of <code>ImageView</code>, this is already a huge improvement. Because of it we can use image loader libraries like <a href="http://square.github.io/picasso/" rel="nofollow"><strong>Picasso</strong></a> directly on the <code>KenBurnsView</code> and we don't have to take care of anything ourselves. You say that you experience random crashes with the <a href="https://github.com/flavioarfaria/KenBurnsView" rel="nofollow"><strong>second library</strong></a>? I have been testing it rather extensively the last few hours and didn't encounter a single crash.</p> <p>With the <code>KenBurnsView</code> from the <a href="https://github.com/flavioarfaria/KenBurnsView" rel="nofollow"><strong>second library</strong></a> and <a href="http://square.github.io/picasso/" rel="nofollow"><strong>Picasso</strong></a> this all becomes very easy and very few lines of code, you just have to create a <code>KenBurnsView</code> for example in xml:</p> <pre><code>&lt;com.flaviofaria.kenburnsview.KenBurnsView android:id="@+id/kbvExample" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/image" /&gt; </code></pre> <p>And then in your <code>Fragment</code> you first have to find the view in the layout and then in <code>onViewCreated()</code> we load the image with Picasso:</p> <pre><code>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); } </code></pre> <h1><strong>5. Testing</strong></h1> <p>I tested everything on my Nexus 5 running Android 4.4.2. Since <code>ViewPropertyAnimators</code> are used this should all be compatible somewhere down to API Level 16, maybe even 12. </p> <p>I have a omitted a few lines of code here and there so if you have any questions feel free to ask!</p> <p>This tip was originally posted on <a href="http://stackoverflow.com/questions/23680066/Implementation%20of%20KenBurns%20effect%20on%20Android%20with%20dynamic%20bitmaps%20setting/23811848">Stack Overflow</a>.</p>
comments powered by Disqus