web-dev-qa-db-fra.com

Est-il possible de demander à la nouvelle classe ImageDecoder de renvoyer des images bitmap, image par image, manuellement?

Contexte

J'essaie de passer manuellement sur des images bitmap de fichiers GIF et WEBP animés (image par image), afin que cela fonctionne non seulement pour Views, mais également dans d'autres cas (comme un fond d'écran animé).

Le problème

Les fichiers GIF animés/WEBP ne sont pris en charge que par Android P, à l’aide de l’API ImageDecoder (exemple ici ).

Pour GIF, je voulais essayer Glide pour la tâche, mais j’ai échoué. J’ai donc essayé de le surmonter en utilisant une bibliothèque qui permet de les charger ( ici , solution ici ). Je pense que ça marche bien.

Pour WebP, je pensais avoir trouvé une autre bibliothèque pouvant fonctionner sur les anciennes versions d'Android ( ici , made fork ici ), mais il semble que cela ne puisse pas gère bien les fichiers WebP dans certains cas (rapporté ici ). J'ai essayé de comprendre quel était le problème et comment le résoudre, mais je n'ai pas réussi.

Donc, en supposant qu'un jour, Google soutiendra l'animation GIF et WEBP pour les anciennes versions d'Android via la bibliothèque de support (ils l'ont écrit ici ), j'ai décidé d'utiliser ImageDecoder pour cette tâche.

En regardant dans l’ensemble de l’API d’ImageDecoder, son utilisation est assez limitée. Je ne vois pas comment je pourrais surmonter ses limites.

Ce que j'ai trouvé

Voici comment utiliser ImageDecoder pour afficher un WebP animé sur un ImageView (juste un exemple, bien sûr, disponible ici ):

class MainActivity : AppCompatActivity() {
    @SuppressLint("StaticFieldLeak")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val source = ImageDecoder.createSource(resources, R.raw.test)
        object : AsyncTask<Void, Void, Drawable?>() {
            override fun doInBackground(vararg params: Void?): Drawable? {
                return try {
                    ImageDecoder.decodeDrawable(source)
                } catch (e: Exception) {
                    null
                }
            }

            override fun onPostExecute(result: Drawable?) {
                super.onPostExecute(result)
                imageView.setImageDrawable(result)
                if (result is AnimatedImageDrawable) {
                    result.start()
                }
            }

        }.execute()

    }
}

J'ai essayé de lire toutes les documentations de ImageDecoder et AnimatedImageDrawable , et aussi de regarder son code, mais je ne vois pas comment il est possible d'aller manuellement sur chaque image et avoir le temps qui s’attend entre eux.

Questions

  1. Existe-t-il un moyen d'utiliser l'API ImageDecoder pour examiner chaque image manuellement, en faisant dessiner un bitmap et en sachant combien de temps il faut attendre entre les images? Toute solution de contournement disponible? Peut-être même en utilisant AnimatedImageDrawable?

  2. J'aimerais faire la même chose sur les anciennes versions d'Android. C'est possible? Si c'est le cas, comment? Peut-être sur une autre API/bibliothèque? Google a écrit que cela fonctionnait de manière à utiliser ImageDecoder sur les anciennes versions d'Android, mais je ne le vois pas mentionné nulle part (à l'exception du lien que j'ai fourni). Probablement pas encore prêt ... Android P n’a même pas encore atteint 0,1% des utilisateurs ... Peut-être Fresco peut le faire? J'ai essayé de le vérifier ici aussi, mais je ne vois pas qu'il soit capable d'une telle chose non plus, et c'est une énorme bibliothèque à utiliser uniquement pour cette tâche, donc je préférerais utiliser une autre bibliothèque à la place. Je sais aussi que libwebp est disponible, mais il est en C/C++ et ne sait pas si cela convient pour Android, et s’il existe un port pour Java/Kotlin pour Android.


MODIFIER:

Étant donné que je pense avoir obtenu ce que je souhaitais, à la fois pour une bibliothèque tierce et pour ImageDecoder, pour pouvoir obtenir des bitmaps à partir de WebP animé, je voudrais toujours savoir comment obtenir le nombre d'images et l'image actuelle à l'aide d'ImageDecoder, si c'est possible. J'ai essayé d'utiliser ImageDecoder.decodeDrawable(source, object : ImageDecoder.OnHeaderDecodedListener..., mais il ne fournit pas d'informations sur le nombre d'images, et il n'y a aucun moyen dans l'API de voir que je peux accéder à un index d'images spécifique et commencer à partir de là, ou de savoir pour combien de temps il faut aller à la prochaine image. J'ai donc posé une question à propos de ceux-ci ici .

Malheureusement, je n'ai pas trouvé non plus que Google dispose d'ImageDecoder pour les anciennes versions d'Android.

C'est également intéressant s'il existe une manière de faire la même chose que je l'ai fait pour le fichier d'animation relativement nouveau de HEIC. Actuellement, il est pris en charge uniquement sur Android P.

17
android developer

OK, j'ai une solution possible en utilisant Glide library , avec GlideWebpDecoder library .

Je ne sais pas si c'est la meilleure façon de le faire, mais je pense que cela devrait bien fonctionner. Le code suivant montre comment il est possible de créer un dessin pouvant être dessiné dans l'occurrence Bitmap que je crée, pour chaque image que l'animation doit afficher. Ce n'est pas exactement ce que j'ai demandé, mais cela pourrait aider les autres.

Voici le code (projet disponible ici ):

CallbackEx.kt

abstract class CallbackEx : Drawable.Callback {
    override fun unscheduleDrawable(who: Drawable, what: Runnable) {}
    override fun invalidateDrawable(who: Drawable) {}
    override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {}
}

MyAppGlideModule.kt

@GlideModule
class MyAppGlideModule : AppGlideModule()

MainActivity.kt

class MainActivity : AppCompatActivity() {
    var webpDrawable: WebpDrawable? = null
    var gifDrawable: GifDrawable? = null
    var callback: Drawable.Callback? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        useFrameByFrameDecoding()
//        useNormalDecoding()
    }

    fun useNormalDecoding() {
        //webp url : https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp
        Glide.with(this)
                //                .load(R.raw.test)
                //                .load(R.raw.fast)
                .load(R.raw.example2)

                //                .load("https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp")
                .into(object : SimpleTarget<Drawable>() {
                    override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
                        imageView.setImageDrawable(drawable)
                        when (drawable) {
                            is GifDrawable -> {
                                drawable.start()
                            }
                            is WebpDrawable -> {
                                drawable.start()
                            }
                        }
                    }
                })
    }

    fun useFrameByFrameDecoding() {
        //webp url : https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp
        Glide.with(this)
                .load(R.raw.test)
                //                .load(R.raw.fast)
                //                .load(R.raw.example2)
                //                .load("https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp")
                .into(object : SimpleTarget<Drawable>() {
                    override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
                        //                        val callback
                        when (drawable) {
                            is GifDrawable -> {
                                gifDrawable = drawable
                                val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
                                val canvas = Canvas(bitmap)
                                drawable.setBounds(0, 0, bitmap.width, bitmap.height)
                                drawable.setLoopCount(GifDrawable.LOOP_FOREVER)
                                callback = object : CallbackEx() {
                                    override fun invalidateDrawable(who: Drawable) {
                                        who.draw(canvas)
                                        imageView.setImageBitmap(bitmap)
                                        Log.d("AppLog", "invalidateDrawable ${drawable.toString().substringAfter('@')} ${drawable.frameIndex}/${drawable.frameCount}")
                                    }
                                }
                                drawable.callback = callback
                                drawable.start()
                            }
                            is WebpDrawable -> {
                                webpDrawable = drawable
                                val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
                                val canvas = Canvas(bitmap)
                                drawable.setBounds(0, 0, bitmap.width, bitmap.height)
                                drawable.setLoopCount(WebpDrawable.LOOP_FOREVER)
                                callback = object : CallbackEx() {
                                    override fun invalidateDrawable(who: Drawable) {
                                        who.draw(canvas)
                                        imageView.setImageBitmap(bitmap)
                                        Log.d("AppLog", "invalidateDrawable ${drawable.toString().substringAfter('@')} ${drawable.frameIndex}/${drawable.frameCount}")
                                    }
                                }
                                drawable.callback = callback
                                drawable.start()
                            }
                        }
                    }
                })
    }

    override fun onStart() {
        super.onStart()
        gifDrawable?.start()
        gifDrawable?.start()
    }

    override fun onStop() {
        super.onStop()
        Log.d("AppLog", "onStop")
        webpDrawable?.stop()
        gifDrawable?.stop()
    }

}

Vous ne savez pas pourquoi SimpleTarget est marqué comme obsolète, et ce que je devrais utiliser à la place, cependant.

En utilisant une technique similaire, j'ai également découvert comment procéder à l'aide d'ImageDecoder, mais pas avec les mêmes fonctionnalités pour une raison quelconque. Un exemple de projet disponible here .

Voici le code:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    var webpDrawable: AnimatedImageDrawable? = null

    @SuppressLint("StaticFieldLeak")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val source = ImageDecoder.createSource(resources, R.raw.test)
        object : AsyncTask<Void, Void, Drawable?>() {
            override fun doInBackground(vararg params: Void?): Drawable? {
                return try {
                    ImageDecoder.decodeDrawable(source)
                } catch (e: Exception) {
                    null
                }
            }

            override fun onPostExecute(drawable: Drawable?) {
                super.onPostExecute(drawable)
//                imageView.setImageDrawable(result)
                if (drawable is AnimatedImageDrawable) {
                    webpDrawable = drawable
                    val bitmap =
                        Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
                    val canvas = Canvas(bitmap)
                    drawable.setBounds(0, 0, bitmap.width, bitmap.height)
                    drawable.repeatCount = AnimatedImageDrawable.REPEAT_INFINITE
                    drawable.callback = object : Drawable.Callback {
                        val handler = Handler()
                        override fun unscheduleDrawable(who: Drawable, what: Runnable) {
                            Log.d("AppLog", "unscheduleDrawable")
                        }

                        override fun invalidateDrawable(who: Drawable) {
                            who.draw(canvas)
                            imageView.setImageBitmap(bitmap)
                            Log.d("AppLog", "invalidateDrawable")
                        }

                        override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
                            Log.d("AppLog", "scheduleDrawable next frame in ${`when` - SystemClock.uptimeMillis()} ms")
                            handler.postAtTime(what, `when`)
                        }
                    }
                    drawable.start()
                }
            }
        }.execute()
    }

    override fun onStart() {
        super.onStart()
        webpDrawable?.start()
    }

    override fun onStop() {
        super.onStop()
        webpDrawable?.stop()
    }

}
4

voir ImageDecoder.Source ...

il faut d’abord créer une source, avec soit:

// source from file
val source = ImageDecoder.createSource(file)

// source from byte buffer
val source = ImageDecoder.createSource(byteBuffer)

// source from resource
val source = ImageDecoder.createSource(resources, resId)

// source from URI
val source = ImageDecoder.createSource(contentResolver, uri)

// source from asset file
val source = ImageDecoder.createSource(assetManager, assetFileName)

puis décodez, avec soit:

// create bitmap
val bitmap = ImageDecoder.decodeBitmap(source)

// create drawable
val drawable = ImageDecoder.decodeDrawable(source)

update: le problème est que le AnimatedImageDrawable résultant n'a pas les deux méthodes suivantes: getNumberOfFrames () et getFrame (int) comme un AnimationDrawable a. comme l'a souligné @androiddeveloper ... j'ai bousillé deux classes différentes. J'ai vérifié la documentation et il semble n'y avoir aucun moyen. avec la GIFImageReader il peut encore être extrait ( source ):

ArrayList<BufferedImage> getFrames(File gif) throws IOException {
    ArrayList<BufferedImage> frames = new ArrayList<BufferedImage>();
    ImageReader ir = new GIFImageReader(new GIFImageReaderSpi());
    ir.setInput(ImageIO.createImageInputStream(gif));
    for(int i = 0; i < ir.getNumImages(true); i++) {
        frames.add(ir.read(i));
    }
    return frames;
}

juste essayé de le convertir en Kotlin, mais javax.imageio.ImageIO n'est pas disponible sur Android.

3
Martin Zeitler

J'ai joué avec des images GIF il y a quelques années. Mon idée est de décoder des images GIF en images, de convertir des images en bitmaps, de créer des dessins animés à partir d’images bitmap et d’attendre entre les images. C'est la classe de décodage:

public class GifDecoder extends Thread {

public static final int STATUS_PARSING = 0;
public static final int STATUS_FORMAT_ERROR = 1;
public static final int STATUS_OPEN_ERROR = 2;
public static final int STATUS_FINISH = -1;
private static final int MaxStackSize = 4096;
public int width; // full image width
public int height; // full image height
int[] lastPixels;
int[] dest;
private InputStream in;
private int status;
private boolean gctFlag; // global color table used
private int gctSize; // size of global color table
private int loopCount = 1; // iterations; 0 = repeat forever
private int[] gct; // global color table
private int[] lct; // local color table
private int[] act; // active color table
private int bgIndex; // background color index
private int bgColor; // background color
private int lastBgColor; // previous bg color
private int pixelAspect; // pixel aspect ratio
private boolean lctFlag; // local color table flag
private boolean interlace; // interlace flag
private int lctSize; // local color table size
private int ix, iy, iw, ih; // current image rectangle
private int lrx, lry, lrw, lrh;
private GifFrame currentFrame = null;
private boolean isShow = false;
private byte[] block = new byte[256]; // current data block
private int blockSize = 0; // block size
private int dispose = 0;
private int lastDispose = 0;
private boolean transparency = false; // use transparent color
// max decoder pixel stack size
private int delay = 0; // delay in milliseconds
private int transIndex; // transparent color index
// LZW decoder working arrays
private short[] prefix;
private byte[] suffix;
private byte[] pixelStack;
private byte[] pixels;
private GifFrame gifFrame; // frames read from current file
private int frameCount;
private GifAction action = null;
private byte[] gifData = null;
private int gifDataOffset;
private int gifDataLength;

private GifDecoder() {

}

public GifDecoder(byte[] data, GifAction act) {
    this(data, 0, data.length, act);
}

public GifDecoder(byte[] data, int offset, int length, GifAction act) {
    gifData = data;
    action = act;
    gifDataOffset = offset;
    gifDataLength = length;
}

public GifDecoder(InputStream is, GifAction act) {
    in = is;
    action = act;
}

public void run() {
    if (in != null) {
        readStream();
    } else if (gifData != null) {
        readByte();
    }
}

public void free() {
    GifFrame fg = gifFrame;
    while (fg != null) {
        if (fg.image != null) {
            fg.image.recycle();
        }
        fg.image = null;
        fg = null;
        gifFrame = gifFrame.nextFrame;
        fg = gifFrame;
    }
    if (in != null) {
        try {
            in.close();
        } catch (Exception ex) {
        }
        in = null;
    }
    gifData = null;
}

public int getStatus() {
    return status;
}

public boolean parseOk() {
    return status == STATUS_FINISH;
}

public int getDelay(int n) {
    delay = -1;
    if ((n >= 0) && (n < frameCount)) {
        GifFrame f = getFrame(n);
        if (f != null) delay = f.delay;
    }
    return delay;
}

public GifFrame getFrame(int n) {
    GifFrame frame = gifFrame;
    int i = 0;
    while (frame != null) {
        if (i == n) {
            return frame;
        } else {
            frame = frame.nextFrame;
        }
        i++;
    }
    return null;
}

public int[] getDelays() {
    GifFrame f = gifFrame;
    int[] d = new int[frameCount];
    int i = 0;
    while (f != null && i < frameCount) {
        d[i] = f.delay;
        f = f.nextFrame;
        i++;
    }
    return d;
}

public int getFrameCount() {
    return frameCount;
}

public Bitmap getImage() {
    return getFrameImage(0);
}

public Bitmap getFrameImage(int n) {
    GifFrame frame = getFrame(n);
    if (frame == null) {
        return null;
    } else {
        return frame.image;
    }
}

public int getLoopCount() {
    return loopCount;
}

public GifFrame getCurrentFrame() {
    return currentFrame;
}

public void reset() {
    currentFrame = gifFrame;
}

public GifFrame next() {
    if (isShow == false) {
        isShow = true;
        return gifFrame;
    } else {
        if (status == STATUS_PARSING) {
            if (currentFrame.nextFrame != null) currentFrame = currentFrame.nextFrame;
            //currentFrame = gifFrame;
        } else {
            currentFrame = currentFrame.nextFrame;
            if (currentFrame == null) {
                currentFrame = gifFrame;
            }
        }
        return currentFrame;
    }
}

private Bitmap setPixels() {
    if (dest == null) dest = new int[width * height];
    // fill in starting image contents based on last image's dispose code
    if (lastDispose > 0) {
        if (lastDispose == 3) {
            // use image before last
            int n = frameCount - 2;
            if (n > 0) {
                Bitmap lastImage = getFrameImage(n - 1);
                if (lastPixels == null) lastPixels = new int[width * height];
                lastImage.getPixels(lastPixels, 0, width, 0, 0, width, height);
            } else {
                lastPixels = null;
            }
        }
        if (lastPixels != null) {
            dest = Arrays.copyOf(lastPixels, lastPixels.length);
            // copy pixels
            if (lastDispose == 2) {
                // fill last image rect area with background color
                int c = 0;
                if (!transparency) {
                    c = lastBgColor;
                }
                for (int i = 0; i < lrh; i++) {
                    int n1 = (lry + i) * width + lrx;
                    int n2 = n1 + lrw;
                    for (int k = n1; k < n2; k++) {
                        dest[k] = c;
                    }
                }
            }
        }
    }

    // copy each source line to the appropriate place in the destination
    int pass = 1;
    int inc = 8;
    int iline = 0;
    for (int i = 0; i < ih; i++) {
        int line = i;
        if (interlace) {
            if (iline >= ih) {
                pass++;
                switch (pass) {
                    case 2:
                        iline = 4;
                        break;
                    case 3:
                        iline = 2;
                        inc = 4;
                        break;
                    case 4:
                        iline = 1;
                        inc = 2;
                }
            }
            line = iline;
            iline += inc;
        }
        line += iy;
        if (line < height) {
            int k = line * width;
            int dx = k + ix; // start of line in dest
            int dlim = dx + iw; // end of dest line
            if ((k + width) < dlim) {
                dlim = k + width; // past dest Edge
            }
            int sx = i * iw; // start of line in source
            while (dx < dlim) {
                // map color and insert in destination
                int index = ((int) pixels[sx++]) & 0xff;
                int c = act[index];
                if (c != 0) {
                    dest[dx] = c;
                }
                dx++;
            }
        }
    }
    return Bitmap.createBitmap(dest, width, height, Config.ARGB_4444);
}

private int readByte() {
    in = new ByteArrayInputStream(gifData, gifDataOffset, gifDataLength);
    gifData = null;
    return readStream();
}

private int readStream() {
    init();
    if (in != null) {
        readHeader();
        if (!err()) {
            readContents();
            if (frameCount < 0) {
                status = STATUS_FORMAT_ERROR;
                action.parseOk(false, -1);
            } else {
                status = STATUS_FINISH;
                action.parseOk(true, -1);
            }
        }
        try {
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else {
        status = STATUS_OPEN_ERROR;
        action.parseOk(false, -1);
    }
    return status;
}

private void decodeImageData() {
    int NullCode = -1;
    int npix = iw * ih;
    int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits,
            code,
            count, i, datum, data_size, first, top, bi, pi;

    if ((pixels == null) || (pixels.length < npix)) {
        pixels = new byte[npix]; // allocate new pixel array
    }
    if (prefix == null) {
        prefix = new short[MaxStackSize];
    }
    if (suffix == null) {
        suffix = new byte[MaxStackSize];
    }
    if (pixelStack == null) {
        pixelStack = new byte[MaxStackSize + 1];
    }
    // Initialize GIF data stream decoder.
    data_size = read();
    clear = 1 << data_size;
    end_of_information = clear + 1;
    available = clear + 2;
    old_code = NullCode;
    code_size = data_size + 1;
    code_mask = (1 << code_size) - 1;
    for (code = 0; code < clear; code++) {
        prefix[code] = 0;
        suffix[code] = (byte) code;
    }

    // Decode GIF pixel stream.
    datum = bits = count = first = top = pi = bi = 0;
    for (i = 0; i < npix; ) {
        if (top == 0) {
            if (bits < code_size) {
                // Load bytes until there are enough bits for a code.
                if (count == 0) {
                    // Read a new data block.
                    count = readBlock();
                    if (count <= 0) {
                        break;
                    }
                    bi = 0;
                }
                datum += (((int) block[bi]) & 0xff) << bits;
                bits += 8;
                bi++;
                count--;
                continue;
            }
            // Get the next code.
            code = datum & code_mask;
            datum >>= code_size;
            bits -= code_size;

            // Interpret the code
            if ((code > available) || (code == end_of_information)) {
                break;
            }
            if (code == clear) {
                // Reset decoder.
                code_size = data_size + 1;
                code_mask = (1 << code_size) - 1;
                available = clear + 2;
                old_code = NullCode;
                continue;
            }
            if (old_code == NullCode) {
                pixelStack[top++] = suffix[code];
                old_code = code;
                first = code;
                continue;
            }
            in_code = code;
            if (code == available) {
                pixelStack[top++] = (byte) first;
                code = old_code;
            }
            while (code > clear) {
                pixelStack[top++] = suffix[code];
                code = prefix[code];
            }
            first = ((int) suffix[code]) & 0xff;
            // Add a new string to the string table,
            if (available >= MaxStackSize) {
                break;
            }
            pixelStack[top++] = (byte) first;
            prefix[available] = (short) old_code;
            suffix[available] = (byte) first;
            available++;
            if (((available & code_mask) == 0) && (available < MaxStackSize)) {
                code_size++;
                code_mask += available;
            }
            old_code = in_code;
        }

        // Pop a pixel off the pixel stack.
        top--;
        pixels[pi++] = pixelStack[top];
        i++;
    }
    for (i = pi; i < npix; i++) {
        pixels[i] = 0; // clear missing pixels
    }
}

private boolean err() {
    return status != STATUS_PARSING;
}

private void init() {
    status = STATUS_PARSING;
    frameCount = 0;
    gifFrame = null;
    gct = null;
    lct = null;
}

private int read() {
    int curByte = 0;
    try {

        curByte = in.read();
    } catch (Exception e) {
        status = STATUS_FORMAT_ERROR;
    }
    return curByte;
}

private int readBlock() {
    blockSize = read();
    int n = 0;
    if (blockSize > 0) {
        try {
            int count = 0;
            while (n < blockSize) {
                count = in.read(block, n, blockSize - n);
                if (count == -1) {
                    break;
                }
                n += count;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (n < blockSize) {
            status = STATUS_FORMAT_ERROR;
        }
    }
    return n;
}

private int[] readColorTable(int ncolors) {
    int nbytes = 3 * ncolors;
    int[] tab = null;
    byte[] c = new byte[nbytes];
    int n = 0;
    try {
        n = in.read(c);
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (n < nbytes) {
        status = STATUS_FORMAT_ERROR;
    } else {
        tab = new int[256]; // max size to avoid bounds checks
        int i = 0;
        int j = 0;
        while (i < ncolors) {
            int r = ((int) c[j++]) & 0xff;
            int g = ((int) c[j++]) & 0xff;
            int b = ((int) c[j++]) & 0xff;
            tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
        }
    }
    return tab;
}

private void readContents() {
    // read GIF file content blocks
    boolean done = false;
    while (!(done || err())) {
        int code = read();
        switch (code) {
            case 0x2C: // image separator
                readImage();
                break;
            case 0x21: // extension
                code = read();
                switch (code) {
                    case 0xf9: // graphics control extension
                        readGraphicControlExt();
                        break;
                    case 0xff: // application extension
                        readBlock();
                        String app = "";
                        for (int i = 0; i < 11; i++) {
                            app += (char) block[i];
                        }
                        if (app.equals("NETSCAPE2.0")) {
                            readNetscapeExt();
                        } else {
                            skip(); // don't care
                        }
                        break;
                    default: // uninteresting extension
                        skip();
                }
                break;
            case 0x3b: // terminator
                done = true;
                break;
            case 0x00: // bad byte, but keep going and see what happens
                break;
            default:
                status = STATUS_FORMAT_ERROR;
        }
    }
}

private void readGraphicControlExt() {
    read(); // block size
    int packed = read(); // packed fields
    dispose = (packed & 0x1c) >> 2; // disposal method
    if (dispose == 0) {
        dispose = 1; // elect to keep old image if discretionary
    }
    transparency = (packed & 1) != 0;
    delay = readShort() * 10; // delay in milliseconds
    transIndex = read(); // transparent color index
    read(); // block terminator
}

private void readHeader() {
    String id = "";
    for (int i = 0; i < 6; i++) {
        id += (char) read();
    }
    if (!id.startsWith("GIF")) {
        status = STATUS_FORMAT_ERROR;
        return;
    }
    readLSD();
    if (gctFlag && !err()) {
        gct = readColorTable(gctSize);
        bgColor = gct[bgIndex];
    }
}

private void readImage() {
    ix = readShort(); // (sub)image position & size
    iy = readShort();
    iw = readShort();
    ih = readShort();
    int packed = read();
    lctFlag = (packed & 0x80) != 0; // 1 - local color table flag
    interlace = (packed & 0x40) != 0; // 2 - interlace flag
    // 3 - sort flag
    // 4-5 - reserved
    lctSize = 2 << (packed & 7); // 6-8 - local color table size
    if (lctFlag) {
        lct = readColorTable(lctSize); // read table
        act = lct; // make local table active
    } else {
        act = gct; // make global table active
        if (bgIndex == transIndex) {
            bgColor = 0;
        }
    }
    int save = 0;
    if (transparency) {
        save = act[transIndex];
        act[transIndex] = 0; // set transparent color if specified
    }
    if (act == null) {
        status = STATUS_FORMAT_ERROR; // no color table defined
    }
    if (err()) {
        return;
    }
    try {
        decodeImageData(); // decode pixel data
        skip();
        if (err()) {
            return;
        }
        frameCount++;
        // create new image to receive frame data
        // createImage(width, height);
        Bitmap image = setPixels(); // transfer pixel data to image
        if (gifFrame == null) {
            gifFrame = new GifFrame(image, delay);
            currentFrame = gifFrame;
        } else {
            GifFrame f = gifFrame;
            while (f.nextFrame != null) {
                f = f.nextFrame;
            }
            f.nextFrame = new GifFrame(image, delay);
        }
        // frames.addElement(new GifFrame(image, delay)); // add image to frame
        // list
        if (transparency) {
            act[transIndex] = save;
        }
        resetFrame();
        if (!action.parseOk(true, frameCount)) {
            status = STATUS_FINISH;
            return;
        }
    } catch (OutOfMemoryError e) {
        Log.e("GifDecoder", ">>> log  : " + e.toString());
        e.printStackTrace();
    }
}

private void readLSD() {
    // logical screen size
    width = readShort();
    height = readShort();
    // packed fields
    int packed = read();
    gctFlag = (packed & 0x80) != 0; // 1 : global color table flag
    // 2-4 : color resolution
    // 5 : gct sort flag
    gctSize = 2 << (packed & 7); // 6-8 : gct size
    bgIndex = read(); // background color index
    pixelAspect = read(); // pixel aspect ratio
}

private void readNetscapeExt() {
    do {
        readBlock();
        if (block[0] == 1) {
            // loop count sub-block
            int b1 = ((int) block[1]) & 0xff;
            int b2 = ((int) block[2]) & 0xff;
            loopCount = (b2 << 8) | b1;
        }
    } while ((blockSize > 0) && !err());
}

private int readShort() {
    // read 16-bit value, LSB first
    return read() | (read() << 8);
}

private void resetFrame() {
    lastDispose = dispose;
    lrx = ix;
    lry = iy;
    lrw = iw;
    lrh = ih;
    lastPixels = dest;
    lastBgColor = bgColor;
    dispose = 0;
    transparency = false;
    delay = 0;
    lct = null;
}

/**
 * Skips variable length blocks up to and including next zero length block.
 */
private void skip() {
    do {
        readBlock();
    } while ((blockSize > 0) && !err());
}

}

Je télécharge la source de démonstration complète ici . J'espère que ça peut t'aider.

2
Louis Solo

EDIT: En implémentant cela, j’ai rencontré quelques problèmes inattendus, mais rien d’insurmontable:

  1. AnimatedImageDrawable semble ignorer ses limites configurées. J'ai redimensionné la toile à la place.
  2. Pour des raisons que je ne comprends pas, AnimatedImageDrawable.draw() omet parfois de programmer la trame suivante. J'ai décidé d'appeler la fonction deux fois. La deuxième fois, je traduis la toile de sorte que tout dessin soit hors limites, ce qui devrait permettre d'optimiser l'essentiel du travail. 

Voici l'exemple de code.

import Android.annotation.*;
import Android.graphics.*;
import Android.graphics.drawable.*;
import Android.os.*;
import Android.service.wallpaper.*;
import Android.support.annotation.*;
import Android.view.*;

@TargetApi(28)
public class TestWallpaper extends WallpaperService
{
    @Override public Engine onCreateEngine()
    {
        return new Engine();
    }

    private class Engine extends WallpaperService.Engine implements Drawable.Callback
    {
        private final Drawable d;
        private final Handler h = new Handler();

        private float scaleX, scaleY;

        private Engine()
        {
            this.setOffsetNotificationsEnabled(false);
            Drawable d = null;
            try
            {
                d = ImageDecoder
                    .decodeDrawable(ImageDecoder.createSource(getResources(), R.drawable.test));
                d.setCallback(this);
                // AnimatedImageDrawable seems to ignore its configured bounds and use its
                // intrinsic bounds instead.
                // In case they fix this bug, we'll go ahead and request the current
                // behavior, and then before drawing we'll transform the canvas to compensate
                d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
                if (d instanceof AnimatedImageDrawable)
                {
                    final AnimatedImageDrawable anim = (AnimatedImageDrawable) d;
                    anim.setRepeatCount(AnimatedImageDrawable.REPEAT_INFINITE);
                    anim.start();
                }
            }
            catch (Throwable t) // should never happen
            {
                t.printStackTrace();
            }
            this.d = d;
        }

        @Override public void invalidateDrawable(@NonNull Drawable _d)
        {
            if(isVisible())
                draw(getSurfaceHolder().getSurface());
        }

        @Override public void scheduleDrawable(@NonNull Drawable _d, @NonNull Runnable _r, long _at)
        {
            if(isVisible())
                h.postAtTime(_r, _d, _at);
        }

        @Override public void unscheduleDrawable(@NonNull Drawable _d, @NonNull Runnable _r)
        {
            h.removeCallbacks(_r, _d);
        }

        @Override public void onSurfaceChanged(SurfaceHolder _sh, int _format, int _w, int _h)
        {
            scaleX = (float) _w / d.getIntrinsicWidth();
            scaleY = (float) _h / d.getIntrinsicHeight();
            draw(_sh.getSurface());
        }

        @Override public void onSurfaceRedrawNeeded(SurfaceHolder _sh)
        {
            draw(_sh.getSurface());
        }

        private void draw(Surface _s)
        {
            try
            {
                final Canvas c = _s.lockCanvas(null);
                c.scale(scaleX, scaleY);
                d.draw(c);
                // Sometimes AnimatedImageDrawable neglects to schedule the next frame
                // after only one draw() of the current frame, so we'll draw() it again,
                // but outside the canvas this time
                c.translate(Float.MAX_VALUE, Float.MAX_VALUE);
                d.draw(c);
                //
                _s.unlockCanvasAndPost(c);
            }
            catch (Throwable t)
            {
                t.printStackTrace();
                // Most likely, the surface was destroyed while we were using it
                // The new one will be delivered to onSurfaceChanged and we'll be fine
            }
        }

        @Override public void onVisibilityChanged(boolean _visible)
        {
            super.onVisibilityChanged(_visible);
            if(_visible)
                draw(getSurfaceHolder().getSurface());
            else
                h.removeCallbacksAndMessages(null);
        }
    }
}
0
j__m