Created
September 3, 2015 21:58
-
-
Save suwhs/3eaf77c8d7d41b7de15b to your computer and use it in GitHub Desktop.
LazyDrawable - abstract class with support preview and full drawable loading on demand, AssetGifDrawable - example implementation that show animated gif, and required classes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2015 whs.su | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package su.whs.watl.samples; | |
import android.content.Context; | |
import android.graphics.Bitmap; | |
import android.graphics.drawable.BitmapDrawable; | |
import android.graphics.drawable.Drawable; | |
import android.os.Build; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import pl.droidsonroids.gif.GifDrawable; | |
import su.whs.watl.experimental.LazyDrawable; | |
import su.whs.watl.samples.utils.GifDecoder; | |
/** | |
* Created by igor n. boulliev on 31.08.15. | |
*/ | |
/** | |
* demo code for suppport animated drawables in TextViewEx (wATL/wATLlib) | |
* gradle dependency : | |
* compile 'pl.droidsonroids.gif:android-gif-drawable:1.1.+' | |
* (and GifDrawableCompat.java for old AOSP version. It's ok to use ONLY GifDrawableCompat - so you can remove | |
* android-gif-drawable dependency and fix code) | |
* | |
* LazyDrawable required for animation on demand launch, | |
* and for support AOSP < 11 | |
* | |
* | |
*/ | |
public class AssetGifDrawable extends LazyDrawable { | |
private String mPath; | |
private Context mContext; | |
private boolean mFullVersionLoaded = false; | |
public AssetGifDrawable(Context context, String path) { | |
super(0, 0); | |
mPath = path; | |
mContext = context; | |
Drawable drawable = readPreviewDrawable(); | |
if (drawable==null) { | |
onFailure(); | |
return; | |
} | |
setSize(drawable.getIntrinsicWidth(),drawable.getIntrinsicHeight()); | |
setDrawable(drawable); | |
} | |
/** | |
* readPreviewDrawable() must returns initial image for DynamicDrawableSpan (less resolution version, BW version), | |
* or single frame from animation | |
* | |
* @return | |
*/ | |
@Override | |
protected Drawable readPreviewDrawable() { | |
if (mLoadingError) return null; | |
GifDecoder decoder = new GifDecoder(); | |
try { | |
InputStream inputStream = mContext.getAssets().open(mPath); | |
decoder.read(inputStream, 0); | |
decoder.advance(); | |
Bitmap frame = decoder.getNextFrame(); | |
Drawable result = new BitmapDrawable(mContext.getResources(),frame); | |
inputStream.close(); | |
return result; | |
} catch (IOException e) { | |
onFailure(); | |
return null; | |
} | |
} | |
/** | |
* readFullDrawable must returns high-quality version of image, or Animatable instance | |
* | |
* @return | |
*/ | |
@Override | |
protected Drawable readFullDrawable() { | |
if (mLoadingError) return null; | |
if (mFullVersionLoaded) return null; | |
try { | |
/* GifDrawable implements Animatable, so we can show animations */ | |
Drawable r = Build.VERSION.SDK_INT>18 ? new GifDrawable(mContext.getAssets().open(mPath)) : new GifDrawableCompat(mContext.getAssets().open(mPath)); | |
mFullVersionLoaded = true; | |
return r; | |
} catch (IOException e) { | |
onFailure(); | |
return null; | |
} | |
} | |
/* call loadFullImage and launch animation */ | |
@Override | |
public void start() { | |
loadFullImage(); | |
super.start(); | |
} | |
public String getPath() { | |
return mPath; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package su.whs.watl.samples.utils; | |
/** | |
* Copyright (c) 2013 Xcellent Creations, Inc. | |
* <p/> | |
* Permission is hereby granted, free of charge, to any person obtaining | |
* a copy of this software and associated documentation files (the | |
* "Software"), to deal in the Software without restriction, including | |
* without limitation the rights to use, copy, modify, merge, publish, | |
* distribute, sublicense, and/or sell copies of the Software, and to | |
* permit persons to whom the Software is furnished to do so, subject to | |
* the following conditions: | |
* <p/> | |
* The above copyright notice and this permission notice shall be | |
* included in all copies or substantial portions of the Software. | |
* <p/> | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
import android.graphics.Bitmap; | |
import android.util.Log; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.nio.BufferUnderflowException; | |
import java.nio.ByteBuffer; | |
import java.nio.ByteOrder; | |
import java.util.ArrayList; | |
/** | |
* Reads frame data from a GIF image source and decodes it into individual frames | |
* for animation purposes. Image data can be read from either and InputStream source | |
* or a byte[]. | |
* | |
* This class is optimized for running animations with the frames, there | |
* are no methods to get individual frame images, only to decode the next frame in the | |
* animation sequence. Instead, it lowers its memory footprint by only housing the minimum | |
* data necessary to decode the next frame in the animation sequence. | |
* | |
* The animation must be manually moved forward using {@link #advance()} before requesting the next | |
* frame. This method must also be called before you request the first frame or an error will | |
* occur. | |
* | |
* Implementation adapted from sample code published in Lyons. (2004). <em>Java for Programmers</em>, | |
* republished under the MIT Open Source License | |
*/ | |
public class GifDecoder { | |
private static final String TAG = GifDecoder.class.getSimpleName(); | |
/** | |
* File read status: No errors. | |
*/ | |
public static final int STATUS_OK = 0; | |
/** | |
* File read status: Error decoding file (may be partially decoded) | |
*/ | |
public static final int STATUS_FORMAT_ERROR = 1; | |
/** | |
* File read status: Unable to open source. | |
*/ | |
public static final int STATUS_OPEN_ERROR = 2; | |
/** | |
* max decoder pixel stack size | |
*/ | |
protected static final int MAX_STACK_SIZE = 4096; | |
/** | |
* GIF Disposal Method meaning take no action | |
*/ | |
private static final int DISPOSAL_UNSPECIFIED = 0; | |
/** | |
* GIF Disposal Method meaning leave canvas from previous frame | |
*/ | |
private static final int DISPOSAL_NONE = 1; | |
/** | |
* GIF Disposal Method meaning clear canvas to background color | |
*/ | |
private static final int DISPOSAL_BACKGROUND = 2; | |
/** | |
* GIF Disposal Method meaning clear canvas to frame before last | |
*/ | |
private static final int DISPOSAL_PREVIOUS = 3; | |
/** | |
* Global status code of GIF data parsing | |
*/ | |
protected int status; | |
//Global File Header values and parsing flags | |
protected int width; // full image width | |
protected int height; // full image height | |
protected boolean gctFlag; // global color table used | |
protected int gctSize; // size of global color table | |
protected int loopCount = 1; // iterations; 0 = repeat forever | |
protected int[] gct; // global color table | |
protected int[] act; // active color table | |
protected int bgIndex; // background color index | |
protected int bgColor; // background color | |
protected int pixelAspect; // pixel aspect ratio | |
protected boolean lctFlag; // local color table flag | |
protected int lctSize; // local color table size | |
// Raw GIF data from input source | |
protected ByteBuffer rawData; | |
// Raw data read working array | |
protected byte[] block = new byte[256]; // current data block | |
protected int blockSize = 0; // block size last graphic control extension info | |
// LZW decoder working arrays | |
protected short[] prefix; | |
protected byte[] suffix; | |
protected byte[] pixelStack; | |
protected byte[] mainPixels; | |
protected int[] mainScratch, copyScratch; | |
protected ArrayList<GifFrame> frames; // frames read from current file | |
protected GifFrame currentFrame; | |
protected Bitmap previousImage, currentImage, renderImage; | |
protected int framePointer; | |
protected int frameCount; | |
/** | |
* Inner model class housing metadata for each frame | |
*/ | |
private static class GifFrame { | |
public int ix, iy, iw, ih; | |
/* Control Flags */ | |
public boolean interlace; | |
public boolean transparency; | |
/* Disposal Method */ | |
public int dispose; | |
/* Transparency Index */ | |
public int transIndex; | |
/* Delay, in ms, to next frame */ | |
public int delay; | |
/* Index in the raw buffer where we need to start reading to decode */ | |
public int bufferFrameStart; | |
/* Local Color Table */ | |
public int[] lct; | |
} | |
/** | |
* Move the animation frame counter forward | |
*/ | |
public void advance() { | |
framePointer = (framePointer + 1) % frameCount; | |
} | |
/** | |
* Gets display duration for specified frame. | |
* | |
* @param n int index of frame | |
* @return delay in milliseconds | |
*/ | |
public int getDelay(int n) { | |
int delay = -1; | |
if ((n >= 0) && (n < frameCount)) { | |
delay = frames.get(n).delay; | |
} | |
return delay; | |
} | |
/** | |
* Gets display duration for the upcoming frame | |
*/ | |
public int getNextDelay() { | |
if (frameCount <= 0 || framePointer < 0) { | |
return -1; | |
} | |
return getDelay(framePointer); | |
} | |
/** | |
* Gets the number of frames read from file. | |
* | |
* @return frame count | |
*/ | |
public int getFrameCount() { | |
return frameCount; | |
} | |
/** | |
* Gets the current index of the animation frame, or -1 if animation hasn't not yet started | |
* | |
* @return frame index | |
*/ | |
public int getCurrentFrameIndex() { | |
return framePointer; | |
} | |
/** | |
* Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely. | |
* | |
* @return iteration count if one was specified, else 1. | |
*/ | |
public int getLoopCount() { | |
return loopCount; | |
} | |
/** | |
* Get the next frame in the animation sequence. | |
* | |
* @return Bitmap representation of frame | |
*/ | |
public Bitmap getNextFrame() { | |
if (frameCount <= 0 || framePointer < 0 || currentImage == null) { | |
return null; | |
} | |
GifFrame frame = frames.get(framePointer); | |
//Set the appropriate color table | |
if (frame.lct == null) { | |
act = gct; | |
} else { | |
act = frame.lct; | |
if (bgIndex == frame.transIndex) { | |
bgColor = 0; | |
} | |
} | |
int save = 0; | |
if (frame.transparency) { | |
save = act[frame.transIndex]; | |
act[frame.transIndex] = 0; // set transparent color if specified | |
} | |
if (act == null) { | |
Log.w(TAG, "No Valid Color Table"); | |
status = STATUS_FORMAT_ERROR; // no color table defined | |
return null; | |
} | |
setPixels(framePointer); // transfer pixel data to image | |
// Reset the transparent pixel in the color table | |
if (frame.transparency) { | |
act[frame.transIndex] = save; | |
} | |
return currentImage; | |
} | |
/** | |
* Reads GIF image from stream | |
* | |
* @param is containing GIF file. | |
* @return read status code (0 = no errors) | |
*/ | |
public int read(InputStream is, int contentLength) { | |
long startTime = System.currentTimeMillis(); | |
if (is != null) { | |
try { | |
int capacity = (contentLength > 0) ? (contentLength + 4096) : 4096; | |
ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity); | |
int nRead; | |
byte[] data = new byte[16384]; | |
while ((nRead = is.read(data, 0, data.length)) != -1) { | |
buffer.write(data, 0, nRead); | |
} | |
buffer.flush(); | |
read(buffer.toByteArray()); | |
// TODO: implement read on demand with recycle bitmaps | |
} catch (IOException e) { | |
Log.w(TAG, "Error reading data from stream", e); | |
} | |
} else { | |
status = STATUS_OPEN_ERROR; | |
} | |
try { | |
is.close(); | |
} catch (Exception e) { | |
Log.w(TAG, "Error closing stream", e); | |
} | |
return status; | |
} | |
/** | |
* Reads GIF image from byte array | |
* | |
* @param data containing GIF file. | |
* @return read status code (0 = no errors) | |
*/ | |
public int read(byte[] data) { | |
init(); | |
if (data != null) { | |
//Initiliaze the raw data buffer | |
rawData = ByteBuffer.wrap(data); | |
rawData.rewind(); | |
rawData.order(ByteOrder.LITTLE_ENDIAN); | |
readHeader(); | |
if (!err()) { | |
readContents(); | |
if (frameCount < 0) { | |
status = STATUS_FORMAT_ERROR; | |
} | |
} | |
} else { | |
status = STATUS_OPEN_ERROR; | |
} | |
return status; | |
} | |
/** | |
* Creates new frame image from current data (and previous frames as specified by their disposition codes). | |
*/ | |
protected void setPixels(int frameIndex) { | |
GifFrame currentFrame = frames.get(frameIndex); | |
GifFrame previousFrame = null; | |
int previousIndex = frameIndex - 1; | |
if (previousIndex >= 0) { | |
previousFrame = frames.get(previousIndex); | |
} | |
// final location of blended pixels | |
final int[] dest = mainScratch; | |
// fill in starting image contents based on last image's dispose code | |
if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) { | |
if (previousFrame.dispose == DISPOSAL_NONE && currentImage != null) { | |
// Start with the current image | |
currentImage.getPixels(dest, 0, width, 0, 0, width, height); | |
} | |
if (previousFrame.dispose == DISPOSAL_BACKGROUND) { | |
// Start with a canvas filled with the background color | |
int c = 0; | |
if (!currentFrame.transparency) { | |
c = bgColor; | |
} | |
for (int i = 0; i < previousFrame.ih; i++) { | |
int n1 = (previousFrame.iy + i) * width + previousFrame.ix; | |
int n2 = n1 + previousFrame.iw; | |
for (int k = n1; k < n2; k++) { | |
dest[k] = c; | |
} | |
} | |
} | |
if (previousFrame.dispose == DISPOSAL_PREVIOUS && previousImage != null) { | |
// Start with the previous frame | |
previousImage.getPixels(dest, 0, width, 0, 0, width, height); | |
} | |
} | |
//Decode pixels for this frame into the global pixels[] scratch | |
decodeBitmapData(currentFrame, mainPixels); // decode pixel data | |
// 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 < currentFrame.ih; i++) { | |
int line = i; | |
if (currentFrame.interlace) { | |
if (iline >= currentFrame.ih) { | |
pass++; | |
switch (pass) { | |
case 2: | |
iline = 4; | |
break; | |
case 3: | |
iline = 2; | |
inc = 4; | |
break; | |
case 4: | |
iline = 1; | |
inc = 2; | |
break; | |
default: | |
break; | |
} | |
} | |
line = iline; | |
iline += inc; | |
} | |
line += currentFrame.iy; | |
if (line < height) { | |
int k = line * width; | |
int dx = k + currentFrame.ix; // start of line in dest | |
int dlim = dx + currentFrame.iw; // end of dest line | |
if ((k + width) < dlim) { | |
dlim = k + width; // past dest edge | |
} | |
int sx = i * currentFrame.iw; // start of line in source | |
while (dx < dlim) { | |
// map color and insert in destination | |
int index = ((int) mainPixels[sx++]) & 0xff; | |
int c = act[index]; | |
if (c != 0) { | |
dest[dx] = c; | |
} | |
dx++; | |
} | |
} | |
} | |
//Copy pixels into previous image | |
currentImage.getPixels(copyScratch, 0, width, 0, 0, width, height); | |
previousImage.setPixels(copyScratch, 0, width, 0, 0, width, height); | |
//Set pixels for current image | |
currentImage.setPixels(dest, 0, width, 0, 0, width, height); | |
} | |
/** | |
* Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. | |
*/ | |
protected void decodeBitmapData(GifFrame frame, byte[] dstPixels) { | |
long startTime = System.currentTimeMillis(); | |
long stepOne, stepTwo, stepThree; | |
if (frame != null) { | |
//Jump to the frame start position | |
rawData.position(frame.bufferFrameStart); | |
} | |
int nullCode = -1; | |
int npix = (frame == null) ? width * height : frame.iw * frame.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 (dstPixels == null || dstPixels.length < npix) { | |
dstPixels = new byte[npix]; // allocate new pixel array | |
} | |
if (prefix == null) { | |
prefix = new short[MAX_STACK_SIZE]; | |
} | |
if (suffix == null) { | |
suffix = new byte[MAX_STACK_SIZE]; | |
} | |
if (pixelStack == null) { | |
pixelStack = new byte[MAX_STACK_SIZE + 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; // XXX ArrayIndexOutOfBoundsException | |
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 >= MAX_STACK_SIZE) { | |
break; | |
} | |
pixelStack[top++] = (byte) first; | |
prefix[available] = (short) old_code; | |
suffix[available] = (byte) first; | |
available++; | |
if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE)) { | |
code_size++; | |
code_mask += available; | |
} | |
old_code = in_code; | |
} | |
// Pop a pixel off the pixel stack. | |
top--; | |
dstPixels[pi++] = pixelStack[top]; | |
i++; | |
} | |
for (i = pi; i < npix; i++) { | |
dstPixels[i] = 0; // clear missing pixels | |
} | |
} | |
/** | |
* Returns true if an error was encountered during reading/decoding | |
*/ | |
protected boolean err() { | |
return status != STATUS_OK; | |
} | |
/** | |
* Initializes or re-initializes reader | |
*/ | |
protected void init() { | |
status = STATUS_OK; | |
frameCount = 0; | |
framePointer = -1; | |
frames = new ArrayList<GifFrame>(); | |
gct = null; | |
} | |
/** | |
* Reads a single byte from the input stream. | |
*/ | |
protected int read() { | |
int curByte = 0; | |
try { | |
curByte = (rawData.get() & 0xFF); | |
} catch (Exception e) { | |
status = STATUS_FORMAT_ERROR; | |
} | |
return curByte; | |
} | |
/** | |
* Reads next variable length block from input. | |
* | |
* @return number of bytes stored in "buffer" | |
*/ | |
protected int readBlock() { | |
blockSize = read(); | |
int n = 0; | |
if (blockSize > 0) { | |
try { | |
int count; | |
while (n < blockSize) { | |
count = blockSize - n; | |
rawData.get(block, n, count); | |
n += count; | |
} | |
} catch (Exception e) { | |
Log.w(TAG, "Error Reading Block", e); | |
status = STATUS_FORMAT_ERROR; | |
} | |
} | |
return n; | |
} | |
/** | |
* Reads color table as 256 RGB integer values | |
* | |
* @param ncolors int number of colors to read | |
* @return int array containing 256 colors (packed ARGB with full alpha) | |
*/ | |
protected int[] readColorTable(int ncolors) { | |
int nbytes = 3 * ncolors; | |
int[] tab = null; | |
byte[] c = new byte[nbytes]; | |
try { | |
rawData.get(c); | |
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; | |
} | |
} catch (BufferUnderflowException e) { | |
Log.w(TAG, "Format Error Reading Color Table", e); | |
status = STATUS_FORMAT_ERROR; | |
} | |
return tab; | |
} | |
/** | |
* Main file parser. Reads GIF content blocks. | |
*/ | |
protected void readContents() { | |
// read GIF file content blocks | |
boolean done = false; | |
while (!(done || err())) { | |
int code = read(); | |
switch (code) { | |
case 0x2C: // image separator | |
readBitmap(); | |
break; | |
case 0x21: // extension | |
code = read(); | |
switch (code) { | |
case 0xf9: // graphics control extension | |
//Start a new frame | |
currentFrame = new GifFrame(); | |
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; | |
case 0xfe:// comment extension | |
skip(); | |
break; | |
case 0x01:// plain text extension | |
skip(); | |
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; | |
} | |
} | |
} | |
/** | |
* Reads GIF file header information. | |
*/ | |
protected 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]; | |
} | |
} | |
/** | |
* Reads Graphics Control Extension values | |
*/ | |
protected void readGraphicControlExt() { | |
read(); // block size | |
int packed = read(); // packed fields | |
currentFrame.dispose = (packed & 0x1c) >> 2; // disposal method | |
if (currentFrame.dispose == 0) { | |
currentFrame.dispose = 1; // elect to keep old image if discretionary | |
} | |
currentFrame.transparency = (packed & 1) != 0; | |
currentFrame.delay = readShort() * 10; // delay in milliseconds | |
currentFrame.transIndex = read(); // transparent color index | |
read(); // block terminator | |
} | |
/** | |
* Reads next frame image | |
*/ | |
protected void readBitmap() { | |
currentFrame.ix = readShort(); // (sub)image position & size | |
currentFrame.iy = readShort(); | |
currentFrame.iw = readShort(); | |
currentFrame.ih = readShort(); | |
int packed = read(); | |
lctFlag = (packed & 0x80) != 0; // 1 - local color table flag interlace | |
lctSize = (int) Math.pow(2, (packed & 0x07) + 1); | |
// 3 - sort flag | |
// 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color | |
// table size | |
currentFrame.interlace = (packed & 0x40) != 0; | |
if (lctFlag) { | |
currentFrame.lct = readColorTable(lctSize); // read table | |
} else { | |
currentFrame.lct = null; //No local color table | |
} | |
currentFrame.bufferFrameStart = rawData.position(); //Save this as the decoding position pointer | |
decodeBitmapData(null, mainPixels); // false decode pixel data to advance buffer | |
skip(); | |
if (err()) { | |
return; | |
} | |
frameCount++; | |
frames.add(currentFrame); // add image to frame | |
} | |
/** | |
* Reads Logical Screen Descriptor | |
*/ | |
protected 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 | |
//Now that we know the size, init scratch arrays | |
mainPixels = new byte[width * height]; | |
mainScratch = new int[width * height]; | |
copyScratch = new int[width * height]; | |
previousImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); | |
currentImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); | |
} | |
/** | |
* Reads Netscape extenstion to obtain iteration count | |
*/ | |
protected 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()); | |
} | |
/** | |
* Reads next 16-bit value, LSB first | |
*/ | |
protected int readShort() { | |
// read 16-bit value | |
return rawData.getShort(); | |
} | |
/** | |
* Skips variable length blocks up to and including next zero length block. | |
*/ | |
protected void skip() { | |
do { | |
readBlock(); | |
} while ((blockSize > 0) && !err()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2015 whs.su | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package su.whs.watl.samples; | |
import android.graphics.Bitmap; | |
import android.graphics.Canvas; | |
import android.graphics.ColorFilter; | |
import android.graphics.Paint; | |
import android.graphics.PixelFormat; | |
import android.graphics.Rect; | |
import android.graphics.drawable.Animatable; | |
import android.graphics.drawable.Drawable; | |
import android.os.SystemClock; | |
import java.io.InputStream; | |
import su.whs.watl.samples.utils.GifDecoder; | |
/** | |
* Created by igor n. boulliev on 01.09.15. | |
*/ | |
public class GifDrawableCompat extends Drawable implements Animatable { | |
private GifDecoder mDecoder; | |
private Bitmap mFrame; | |
private boolean mStarted = false; | |
private Paint mPaint = new Paint(); | |
private Rect mSrcRect = new Rect(); | |
private Runnable updateRunable = new Runnable() { | |
@Override | |
public void run() { | |
nextFrame(); | |
if (mStarted) | |
scheduleSelf(updateRunable, SystemClock.uptimeMillis()+mDecoder.getNextDelay()); | |
} | |
}; | |
public GifDrawableCompat(InputStream inputStream) { | |
mDecoder = new GifDecoder(); | |
mDecoder.read(inputStream,0); | |
mDecoder.advance(); | |
mFrame = mDecoder.getNextFrame(); | |
mSrcRect.set(0,0,mFrame.getWidth(),mFrame.getHeight()); | |
} | |
@Override | |
public int getIntrinsicWidth() { return mSrcRect.width(); } | |
@Override | |
public int getIntrinsicHeight() { return mSrcRect.height(); } | |
private void nextFrame() { | |
mDecoder.advance(); | |
mFrame = mDecoder.getNextFrame(); | |
invalidateSelf(); | |
} | |
@Override | |
public void start() { | |
mStarted = true; | |
scheduleSelf(updateRunable,SystemClock.uptimeMillis()+mDecoder.getNextDelay()); | |
} | |
@Override | |
public void stop() { | |
unscheduleSelf(updateRunable); | |
mStarted = false; | |
} | |
@Override | |
public boolean isRunning() { | |
return mStarted; | |
} | |
@Override | |
public void draw(Canvas canvas) { | |
canvas.drawBitmap(mFrame,mSrcRect,getBounds(),mPaint); | |
} | |
@Override | |
public void setAlpha(int alpha) { | |
mPaint.setAlpha(alpha); | |
} | |
@Override | |
public void setColorFilter(ColorFilter cf) { | |
mPaint.setColorFilter(cf); | |
} | |
@Override | |
public int getOpacity() { | |
return PixelFormat.TRANSPARENT; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2015 whs.su | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package su.whs.watl.experimental; | |
import android.content.res.Resources; | |
import android.graphics.Bitmap; | |
import android.graphics.BitmapFactory; | |
import android.graphics.Canvas; | |
import android.graphics.ColorFilter; | |
import android.graphics.Rect; | |
import android.graphics.drawable.Animatable; | |
import android.graphics.drawable.BitmapDrawable; | |
import android.graphics.drawable.Drawable; | |
import android.os.Build; | |
import android.os.Handler; | |
import android.os.Looper; | |
import android.os.SystemClock; | |
import java.io.IOException; | |
import java.net.MalformedURLException; | |
import java.net.URL; | |
import java.util.concurrent.LinkedBlockingQueue; | |
import java.util.concurrent.ThreadPoolExecutor; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* Created by igor n. boulliev on 29.08.15. | |
*/ | |
/** | |
* abstract class for support two display mode - 'preview' and 'full' | |
* for example, it may display local cached jpeg as preview, and load high quality png on | |
* request | |
* | |
*/ | |
public abstract class LazyDrawable extends Drawable implements Animatable, Drawable.Callback { | |
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(1,3,10, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>()); | |
private static final String TAG="LazyDrawable"; | |
private int mSrcWidth; | |
private int mSrcHeight; | |
private Drawable mDrawable; | |
private Rect mBounds = new Rect(); | |
private boolean mBoundsApplied = false; | |
private boolean mLoadingInProgress = false; | |
protected boolean mLoadingError = false; | |
private Drawable.Callback mCallbackCompat = null; | |
/** | |
* create instance with given size | |
* @param srcWidth | |
* @param srcHeight | |
*/ | |
public LazyDrawable(int srcWidth, int srcHeight) { | |
mSrcWidth = srcWidth; | |
mSrcHeight = srcHeight; | |
mBounds.set(0, 0, srcWidth, srcHeight); | |
} | |
@Override | |
public int getIntrinsicWidth() { | |
return mSrcWidth; | |
} | |
@Override | |
public int getIntrinsicHeight() { | |
return mSrcHeight; | |
} | |
@Override | |
public void setBounds(int left, int top, int right, int bottom) { | |
mBoundsApplied = false; | |
mBounds.set(left,top,right,bottom); | |
super.setBounds(left,top,right,bottom); | |
} | |
/** | |
* set size and adjust bounds | |
* @param srcWidth | |
* @param srcHeight | |
*/ | |
protected void setSize(int srcWidth, int srcHeight) { | |
mSrcWidth = srcWidth; | |
mSrcHeight = srcHeight; | |
mBounds.right = mBounds.left + srcWidth; | |
mBounds.bottom = mBounds.top + srcHeight; | |
mBoundsApplied = false; | |
} | |
/** | |
* load preview (sync) | |
*/ | |
public void initialLoad() { | |
Drawable d = readPreviewDrawable(); | |
synchronized (LazyDrawable.this) { | |
mDrawable = d; | |
mLoadingInProgress = false; | |
} | |
} | |
/** | |
* if no preview loaded - execute initialLoad() in background and draw 'loadingFrame', if defined | |
* @param canvas | |
*/ | |
@Override | |
public void draw(Canvas canvas) { | |
if (mDrawable==null) { | |
synchronized (this) { | |
mLoadingInProgress = true; | |
executor.execute(new Runnable() { | |
@Override | |
public void run() { | |
initialLoad(); | |
invalidateSelfOnUiThread(); | |
} | |
}); | |
} | |
} | |
if (!mBoundsApplied) { | |
synchronized (this) { | |
mDrawable.setBounds(mBounds); | |
mBoundsApplied = true; | |
} | |
} | |
boolean loadError; | |
boolean loadingInProgress; | |
synchronized (this) { | |
if (mDrawable!=null) | |
mDrawable.draw(canvas); | |
loadError = mLoadingError; | |
loadingInProgress = mLoadingInProgress; | |
} | |
if (loadingInProgress) { | |
/* draw loading animation */ | |
drawNextLoadingFrame(canvas); | |
/* */ | |
if (getCallbackCompat()!=null) { | |
scheduleSelf(new Runnable() { | |
@Override | |
public void run() { | |
invalidateSelf(); | |
} | |
}, SystemClock.uptimeMillis()+100); | |
} | |
} else if (loadError) { | |
/* draw loading error sign */ | |
} | |
} | |
/** | |
* by default - do nothing. Override it to draw loading animation | |
* @param canvas | |
*/ | |
protected void drawNextLoadingFrame(Canvas canvas) { | |
} | |
/** | |
* initiate load full image (sync) | |
*/ | |
public void loadFullImage() { | |
synchronized (this) { | |
mLoadingError = false; | |
mLoadingInProgress = true; | |
} | |
executor.execute( | |
new Runnable() { | |
@Override | |
public void run() { | |
Drawable drawable = readFullDrawable(); | |
synchronized (LazyDrawable.this) { | |
if (drawable != null) | |
setDrawable(drawable); | |
} | |
invalidateSelfOnUiThread(); | |
} | |
} | |
); | |
} | |
@Override | |
public void setAlpha(int alpha) { | |
synchronized (this) { | |
mDrawable.setAlpha(alpha); | |
} | |
} | |
@Override | |
public void setColorFilter(ColorFilter cf) { | |
synchronized (this) { | |
mDrawable.setColorFilter(cf); | |
} | |
} | |
@Override | |
public int getOpacity() { | |
synchronized (this) { | |
return mDrawable.getOpacity(); | |
} | |
} | |
@Override | |
public void start() { | |
synchronized (this) { | |
if (mDrawable instanceof Animatable) { | |
((Animatable)mDrawable).start(); | |
} | |
} | |
} | |
@Override | |
public void stop() { | |
synchronized (this) { | |
if (mDrawable instanceof Animatable) { | |
((Animatable)mDrawable).stop(); | |
} | |
} | |
} | |
@Override | |
public boolean isRunning() { | |
synchronized (this) { | |
if (mDrawable instanceof Animatable) { | |
return ((Animatable)mDrawable).isRunning(); | |
} | |
} | |
return false; | |
} | |
/** | |
* replace wrapped drawable (and reset internal flags) | |
* @param drawable | |
*/ | |
protected synchronized void setDrawable(Drawable drawable) { | |
if (mDrawable!=null && isRunning()) stop(); | |
mDrawable = drawable; | |
mDrawable.setCallback(this); | |
mBoundsApplied = false; | |
} | |
/** | |
* set unrecoverable load error flag | |
*/ | |
protected void onFailure() { | |
mLoadingError = true; | |
invalidateSelfOnUiThread(); | |
} | |
@Override | |
public void invalidateDrawable(Drawable who) { | |
if (getCallbackCompat()!=null) | |
getCallbackCompat().invalidateDrawable(this); | |
} | |
@Override | |
public void scheduleDrawable(Drawable who, Runnable what, long when) { | |
if (getCallbackCompat()!=null) | |
getCallbackCompat().scheduleDrawable(this, what, when); | |
} | |
@Override | |
public void unscheduleDrawable(Drawable who, Runnable what) { | |
if (getCallbackCompat()!=null) | |
getCallbackCompat().unscheduleDrawable(this, what); | |
} | |
private Drawable.Callback getCallbackCompat() { | |
if (Build.VERSION.SDK_INT>10) return getCallback(); | |
return mCallbackCompat; | |
} | |
public void setCallbackCompat(Drawable.Callback cb) { | |
if (Build.VERSION.SDK_INT>10) { | |
setCallback(cb); | |
return; | |
} | |
mCallbackCompat = cb; | |
} | |
/** | |
* run invalidateSelf() on MainLooper | |
*/ | |
protected void invalidateSelfOnUiThread() { | |
if (Looper.getMainLooper().getThread().equals(Thread.currentThread())) { | |
invalidateSelf(); | |
} else { | |
new Handler(Looper.getMainLooper()).post(new Runnable() { | |
@Override | |
public void run() { | |
LazyDrawable.this.invalidateSelf(); | |
} | |
}); | |
} | |
} | |
/** | |
* abstract method. Implementation must return Drawable fast as possible | |
* @return | |
*/ | |
protected abstract Drawable readPreviewDrawable(); | |
/** | |
* abstract method. Implementation may load real image from network (not fast), | |
* store to cache (and use it when this drawable required again) | |
* @return | |
*/ | |
protected abstract Drawable readFullDrawable(); | |
/** | |
* sample implementation - only 'slow method' | |
* generally - for development and debug purposes | |
*/ | |
public static class FromURL extends LazyDrawable { | |
private String mSrcUrl; | |
public FromURL(String srcUrl) throws IOException { | |
super(0, 0); | |
mSrcUrl = srcUrl; | |
Drawable drawable = readFullDrawable(); | |
setDrawable(drawable); | |
} | |
protected FromURL(int srcWidth, int srcHeight) { | |
super(srcWidth, srcHeight); | |
} | |
protected FromURL(int srcWidth, int srcHeight, String srcUrl) { | |
super(srcWidth,srcHeight); | |
mSrcUrl = srcUrl; | |
} | |
public String getURL() { return mSrcUrl; } | |
@Override | |
protected Drawable readPreviewDrawable() { | |
return readFullDrawable(); | |
} | |
@Override | |
protected Drawable readFullDrawable() { | |
URL url; | |
try { | |
url = new URL(mSrcUrl); | |
} catch (MalformedURLException e) { | |
onFailure(); | |
return null; | |
} | |
Bitmap image = null; | |
try { | |
image = BitmapFactory.decodeStream(url.openConnection().getInputStream()); | |
} catch (IOException e) { | |
onFailure(); | |
return null; | |
} | |
setSize(image.getWidth(),image.getHeight()); | |
return new BitmapDrawable(Resources.getSystem(),image); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment