31

Background

I have a small live wallpaper app, that I want to add support for it to show GIF animations.

For this, I've found various solutions. There is the solution of showing a GIF animation in a view (here), and there is even a solution for showing it in a live wallpaper (here).

However, for both of them, I can't find how to fit the content of the GIF animation nicely in the space it has, meaning any of the following:

  1. center-crop - fits to 100% of the container (the screen in this case), cropping on sides (top&bottom or left&right) when needed. Doesn't stretch anything. This means the content seems fine, but not all of it might be shown.
  2. fit-center - stretch to fit width/height
  3. center-inside - set as original size, centered, and stretch to fit width/height only if too large.

The problem

None of those is actually about ImageView, so I can't just use the scaleType attribute.

What I've found

There is a solution that gives you a GifDrawable (here), which you can use in ImageView, but it seems it's pretty slow in some cases, and I can't figure out how to use it in LiveWallpaper and then fit it.

The main code of the LiveWallpaper GIF handling is as such (here) :

class GIFWallpaperService : WallpaperService() {
    override fun onCreateEngine(): WallpaperService.Engine {
        val movie = Movie.decodeStream(resources.openRawResource(R.raw.cinemagraphs))
        return GIFWallpaperEngine(movie)
    }

    private inner class GIFWallpaperEngine(private val movie: Movie) : WallpaperService.Engine() {
        private val frameDuration = 20

        private var holder: SurfaceHolder? = null
        private var visible: Boolean = false
        private val handler: Handler = Handler()
        private val drawGIF = Runnable { draw() }

        private fun draw() {
            if (visible) {
                val canvas = holder!!.lockCanvas()
                canvas.save()
                movie.draw(canvas, 0f, 0f)
                canvas.restore()
                holder!!.unlockCanvasAndPost(canvas)
                movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())

                handler.removeCallbacks(drawGIF)
                handler.postDelayed(drawGIF, frameDuration.toLong())
            }
        }

        override fun onVisibilityChanged(visible: Boolean) {
            this.visible = visible
            if (visible)
                handler.post(drawGIF)
            else
                handler.removeCallbacks(drawGIF)
        }

        override fun onDestroy() {
            super.onDestroy()
            handler.removeCallbacks(drawGIF)
        }

        override fun onCreate(surfaceHolder: SurfaceHolder) {
            super.onCreate(surfaceHolder)
            this.holder = surfaceHolder
        }
    }
}

The main code for handling GIF animation in a view is as such:

class CustomGifView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
    private var gifMovie: Movie? = null
    var movieWidth: Int = 0
    var movieHeight: Int = 0
    var movieDuration: Long = 0
    var mMovieStart: Long = 0

    init {
        isFocusable = true
        val gifInputStream = context.resources.openRawResource(R.raw.test)

        gifMovie = Movie.decodeStream(gifInputStream)
        movieWidth = gifMovie!!.width()
        movieHeight = gifMovie!!.height()
        movieDuration = gifMovie!!.duration().toLong()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(movieWidth, movieHeight)
    }

    override fun onDraw(canvas: Canvas) {

        val now = android.os.SystemClock.uptimeMillis()
        if (mMovieStart == 0L) {   // first time
            mMovieStart = now
        }
        if (gifMovie != null) {
            var dur = gifMovie!!.duration()
            if (dur == 0) {
                dur = 1000
            }
            val relTime = ((now - mMovieStart) % dur).toInt()
            gifMovie!!.setTime(relTime)
            gifMovie!!.draw(canvas, 0f, 0f)
            invalidate()
        }
    }
}

The questions

  1. Given a GIF animation, how can I scale it in each of the above ways?
  2. Is it possible to have a single solution for both cases?
  3. Is it possible to use GifDrawable library (or any other drawable for the matter) for the live wallpaper, instead of the Movie class? If so, how?

EDIT: after finding how to scale for 2 kinds, I still need to know how to scale according to the third type, and also want to know why it keeps crashing after orientation changes, and why it doesn't always show the preview right away.

I'd also like to know what's the best way to show the GIF animation here, because currently I just refresh the canvas ~60fps (1000/60 waiting between each 2 frames), without consideration of what's in the file.

Project is available here.

android developer
  • 114,585
  • 152
  • 739
  • 1,270

3 Answers3

6

If you have Glide in your project, You can easily load Gifs, as it provides drawing GIFs to your ImageViews and does support many scaling options (like center or a given width and ...).

Glide.with(context)
    .load(imageUrl or resourceId)
    .asGif()
    .fitCenter() //or other scaling options as you like
    .into(imageView);
Adib Faramarzi
  • 3,798
  • 3
  • 29
  • 44
  • 1
    I can use Glide for GIF animation? Also, your code doesn't make sense, because there is no imageView here (there's not even a layout). It's a live wallpaper. Please show how to do it in a live wallpaper – android developer May 18 '18 at 14:55
  • Yes you can use Glide for GIFs as well as static images. For the wallpaper part, instead of using an imageView for the `into` part, you can have a callback to retrieve the GIF and use [this answer](https://stackoverflow.com/a/9160389/450356) to set the wallpaper. – Adib Faramarzi May 18 '18 at 15:22
  • I don't see how Glide fits in this link. – android developer May 18 '18 at 21:41
  • Glide can load and resize the GIF and in it's callback (using `.into(Target);` you can use that to pass the GIF to WallpaperManager using the link I provided. – Adib Faramarzi May 19 '18 at 05:32
  • Please show full sample code. I remember I've tried this and it didn't work for me. Also, are you sure GIF is handled well using Glide? No memory issues even if the file is quite large? – android developer May 19 '18 at 08:45
  • Yes, Glide handles memory issues very well (compared to others like Picasso). – Adib Faramarzi May 19 '18 at 15:26
  • I meant more in comparison to other GIF solutions, such as the Movie class and the GifDrawable library (here: https://github.com/koral--/android-gif-drawable ) – android developer May 19 '18 at 20:20
  • I don't know about GifDrawable but Glide is a full packaged library and has one of the cleanest codebases in android libraries. I've used it with very large images and it handles them very smoothly. – Adib Faramarzi May 20 '18 at 04:58
  • ok please show how to use it in live wallpaper using a full sample code so that I could grant you the bounty – android developer May 20 '18 at 06:10
3

OK I think I got how to scale the content. Not sure though why the app still crashes upon orientation change sometimes, and why the app doesn't show the preview right away sometimes.

Project is available here.

For center-inside, the code is:

    private fun draw() {
        if (!isVisible)
            return
        val canvas = holder!!.lockCanvas() ?: return
        canvas.save()
        //center-inside
        val scale = Math.min(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
        val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
        val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
        canvas.translate(x, y)
        canvas.scale(scale, scale)
        movie.draw(canvas, 0f, 0f)
        canvas.restore()
        holder!!.unlockCanvasAndPost(canvas)
        movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
        handler.removeCallbacks(drawGIF)
        handler.postDelayed(drawGIF, frameDuration.toLong())
    }

For center-crop, the code is:

    private fun draw() {
        if (!isVisible)
            return
        val canvas = holder!!.lockCanvas() ?: return
        canvas.save()
        //center crop
        val scale = Math.max(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
        val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
        val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
        canvas.translate(x, y)
        canvas.scale(scale, scale)
        movie.draw(canvas, 0f, 0f)
        canvas.restore()
        holder!!.unlockCanvasAndPost(canvas)
        movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
        handler.removeCallbacks(drawGIF)
        handler.postDelayed(drawGIF, frameDuration.toLong())
    }

for fit-center, I can use this:

                    val canvasWidth = canvas.width.toFloat()
                    val canvasHeight = canvas.height.toFloat()
                    val bitmapWidth = curBitmap.width.toFloat()
                    val bitmapHeight = curBitmap.height.toFloat()
                    val scaleX = canvasWidth / bitmapWidth
                    val scaleY = canvasHeight / bitmapHeight
                    scale = if (scaleX * curBitmap.height > canvas.height) scaleY else scaleX
                    x = (canvasWidth / 2f) - (bitmapWidth / 2f) * scale
                    y = (canvasHeight / 2f) - (bitmapHeight / 2f) * scale
                    ...
android developer
  • 114,585
  • 152
  • 739
  • 1,270
2

Change the width and the height of the movie:

Add this code in onDraw method before movie.draw

canvas.scale((float)this.getWidth() / (float)movie.width(),(float)this.getHeight() /      (float)movie.height());

or

canvas.scale(1.9f, 1.21f); //this changes according to screen size

Scale to fill and scale to fit:

There's already a good answer on that:

https://stackoverflow.com/a/38898699/5675325

Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145
  • 1
    Which scaling type is this? Can you please tell how to do the others too? – android developer Jun 06 '18 at 08:12
  • Again, which of the ones of the scaling types I've mentioned is this? To me it seems you chose to stretch it, meaning the aspect ratio isn't saved, and this means none of what I've written I wish to achieve. Shouldn't I also cause the canvas to move? About the link, The one they called "Scale to fit" seems like fit-center, but it puts the content on wrong place for me (bottom instead of center in portrait). The one called "Scaling to fill" is center-crop, but I can't even see it being drawn (on portrait). Please try it out . – android developer Jun 06 '18 at 19:01