├── .gitignore ├── .idea ├── caches │ ├── build_file_checksums.ser │ └── gradle_models.ser ├── codeStyles │ └── Project.xml ├── compiler.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── apng ├── .gitignore ├── build.gradle ├── libs │ ├── commons-io-2.4.jar │ └── pngj-2.1.1.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── test │ │ └── sakhu │ │ └── com │ │ └── apng │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── apng │ │ │ ├── ApngACTLChunk.java │ │ │ ├── ApngChunk.java │ │ │ ├── ApngConst.java │ │ │ ├── ApngDataChunk.java │ │ │ ├── ApngDataSupplier.java │ │ │ ├── ApngFCTLChunk.java │ │ │ ├── ApngFrame.java │ │ │ ├── ApngFrameRender.java │ │ │ ├── ApngIHDRChunk.java │ │ │ ├── ApngMmapParserChunk.java │ │ │ ├── ApngPaserChunk.java │ │ │ ├── ApngReader.java │ │ │ ├── ByteUtil.java │ │ │ ├── Fdat2IdatChunk.java │ │ │ ├── FormatNotSupportException.java │ │ │ ├── PngStream.java │ │ │ ├── entity │ │ │ └── AnimParams.java │ │ │ ├── utils │ │ │ ├── ApngDownloadUtil.java │ │ │ ├── ApngUtils.java │ │ │ ├── FileUtils.java │ │ │ ├── Md5.java │ │ │ └── RecyclingUtils.java │ │ │ └── view │ │ │ ├── ApngImageView.java │ │ │ ├── ApngLoader.java │ │ │ └── ApngSurfaceView.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── test │ └── sakhu │ └── com │ └── apng │ └── ExampleUnitTest.java ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── saku │ │ └── test │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── car.png │ │ └── color_ball.png │ ├── java │ │ └── com │ │ │ └── saku │ │ │ └── test │ │ │ ├── ApngImageViewActivity.java │ │ │ ├── ApngSurfaceViewActivity.java │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ ├── activity_image_view.xml │ │ ├── activity_main.xml │ │ └── activity_surface_view.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── saku │ └── test │ └── ExampleUnitTest.java ├── build.gradle ├── demo.mp4 ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/caches/gradle_models.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/.idea/caches/gradle_models.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SakuApng 2 | Apng的解析器和播放器 3 | 4 | # 1. 概述 5 | 在深入了解Apng动画播放之前,我们需要对Apng的结构有所了解,具体参见[**Apng动画介绍**](http://www.jianshu.com/p/5333bcc20ba7),对Apng的整体结构有所了解后,下面我们来讲讲Apng动画的播放,主要包括Apng解析和Apng渲染两个过程。 6 | # 2. Apng动画播放流程 7 | Apng动画播放流程包括Apng解析和Apng渲染两个过程,Apng解析主要有两种方法,下面我们将会介绍,而Apng渲染主要包括三个步骤:**消除(dispose)、合成(blend)、绘制(draw)**,由此得到Apng动画播放流程图如下: 8 | ![Apng动画播放流程](http://upload-images.jianshu.io/upload_images/3427834-aa0b26e40be1d556.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 9 | # 3. Apng的解析 10 | Apng的解析主要是将Apng文件转化成Apng序列帧Frame-n,从上面的流程图可知,Apng文件的解析列出了两种方案,下面来分别说说: 11 | 12 | 1)Apng文件首先经过一个解压(**ApngExact**)的过程,生成png序列帧保存在本地,然后经过加载(**LoadPng**)处理生成序列帧Frame-n。 13 | 14 | 假设Apng动画文件总共有90帧,那么经过ApngExact处理后,会生成90张png序列帧保存在本地,每帧通过LoadPng处理生成Bitmap并供后面的Apng渲染使用。 15 | 16 | 2)Apng是一个独立的文件,我们自己编写读取Apng文件的代码类:ApngReader,当渲染第i帧时,通过ApngReader直接获取第i帧的Bitmap。 17 | 18 | **比较:** 19 | 20 | 1)方案一是将Apng文件全部解压成png序列图片保存在本地,方案二是把Apng文件当做一个整体去处理,需要第几帧直接读取第几帧,并将该帧以Bitmap的形似保存到内存。 21 | 22 | 2)方案一解压得到的png图片在后面的渲染中需要转化成Bitamp,而方案二直接就获取了第几帧的Bitmap,相比于方案一,方案二减少了一个从SD卡读取png文件的操作。 23 | 24 | # ApngReader的实现 25 | 26 | 方案一的具体实现大家可以参考github上面的一个项目[**apng-view**](https://github.com/sahasbhop/apng-view),方案二的具体实现,即ApngReader的实现参见简书[**Android-Apng动画的播放**](https://www.jianshu.com/p/8114ed31c535)。 27 | 28 | -------------------------------------------------------------------------------- /apng/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /apng/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | 6 | 7 | 8 | defaultConfig { 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /apng/libs/commons-io-2.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/apng/libs/commons-io-2.4.jar -------------------------------------------------------------------------------- /apng/libs/pngj-2.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/apng/libs/pngj-2.1.1.jar -------------------------------------------------------------------------------- /apng/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /apng/src/androidTest/java/test/sakhu/com/apng/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package test.sakhu.com.apng; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("test.sakhu.com.apng.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apng/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngACTLChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * ACTL chunk 5 | * 6 | * @author ltf 7 | * @since 16/11/28, 下午12:09 8 | */ 9 | public class ApngACTLChunk extends ApngDataChunk { 10 | private int numFrames; 11 | private int numPlays; 12 | 13 | public int getNumPlays() { 14 | return numPlays; 15 | } 16 | 17 | public int getNumFrames() { 18 | return numFrames; 19 | } 20 | 21 | protected void parseData(ApngDataSupplier data) { 22 | this.numFrames = data.readInt(); 23 | this.numPlays = data.readInt(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * Base APng Chunk Object 5 | * 6 | * @author ltf 7 | * @since 16/11/26, 上午11:19 8 | */ 9 | abstract class ApngChunk { 10 | 11 | public int getLength() { 12 | return length; 13 | } 14 | 15 | public int getTypeCode() { 16 | return typeCode; 17 | } 18 | 19 | public int getCrc() { 20 | return crc; 21 | } 22 | 23 | // data sections 24 | protected int length; 25 | protected int typeCode; 26 | protected int crc; 27 | 28 | 29 | ApngChunk() { 30 | 31 | } 32 | 33 | ApngChunk(ApngChunk copyFrom) { 34 | this.length = copyFrom.length; 35 | this.typeCode = copyFrom.typeCode; 36 | this.crc = copyFrom.crc; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngConst.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * Apng Const 5 | * 6 | * @author ltf 7 | * @since 16/11/26, 下午3:26 8 | */ 9 | public class ApngConst { 10 | // signature 11 | public static final int PNG_SIG = -1991225785; 12 | public static final int PNG_SIG_VER = 218765834; 13 | 14 | // type code 15 | public static final int CODE_IHDR = 1229472850; 16 | 17 | public static final int CODE_iCCP = 1347179589; 18 | public static final int CODE_sRGB = 1934772034; 19 | public static final int CODE_sBIT = 1933723988; 20 | public static final int CODE_gAMA = 1732332865; 21 | public static final int CODE_cHRM = 1665684045; 22 | 23 | public static final int CODE_PLTE = 1347179589; 24 | 25 | public static final int CODE_tRNS = 1951551059; 26 | public static final int CODE_hIST = 1749635924; 27 | public static final int CODE_bKGD = 1649100612; 28 | public static final int CODE_pHYs = 1883789683; 29 | public static final int CODE_sPLT = 1934642260; 30 | 31 | public static final int CODE_acTL = 1633899596; 32 | public static final int CODE_fcTL = 1717785676; 33 | public static final int CODE_IDAT = 1229209940; 34 | public static final int CODE_fdAT = 1717846356; 35 | public static final int CODE_IEND = 1229278788; 36 | 37 | // ".ang" format [ self extended apng format, optimized for speed, quality and size ] 38 | //public static final int CODE_fcRC = 1717785155; 39 | } 40 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngDataChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * Apng Chunk as data container 5 | * 6 | * @author ltf 7 | * @since 16/11/29, 下午12:16 8 | */ 9 | public abstract class ApngDataChunk extends ApngChunk { 10 | 11 | public void parse(ApngDataSupplier data) { 12 | length = data.readInt(); 13 | typeCode = data.readInt(); 14 | parseData(data); 15 | this.crc = data.readInt(); 16 | } 17 | 18 | protected void parseData(ApngDataSupplier data) { 19 | data.move(length); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngDataSupplier.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * @author ltf 5 | * @since 16/11/29, 下午12:32 6 | */ 7 | public interface ApngDataSupplier { 8 | 9 | /** 10 | * read int from data and move the pointer 4 byte ahead 11 | */ 12 | int readInt(); 13 | 14 | /** 15 | * read int from data and move the pointer 2 byte ahead 16 | */ 17 | short readShort(); 18 | 19 | /** 20 | * read int from data and move the pointer 1 byte ahead 21 | */ 22 | byte readByte(); 23 | 24 | /** 25 | * move the pointer ahead by distance bytes 26 | */ 27 | void move(int distance); 28 | } 29 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngFCTLChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * FCTL Chunk 5 | * 6 | * @author ltf 7 | * @since 16/11/28, 下午12:10 8 | */ 9 | public class ApngFCTLChunk extends ApngDataChunk { 10 | 11 | public static final byte APNG_DISPOSE_OP_NONE = 0; 12 | public static final byte APNG_DISPOSE_OP_BACKGROUND = 1; 13 | public static final byte APNG_DISPOSE_OP_PREVIOUS = 2; 14 | public static final byte APNG_BLEND_OP_SOURCE = 0; 15 | public static final byte APNG_BLEND_OP_OVER = 1; 16 | private int seqNum; 17 | private int width; 18 | private int height; 19 | private int xOff; 20 | private int yOff; 21 | private int delayNum; 22 | private int delayDen; 23 | private byte disposeOp; 24 | private byte blendOp; 25 | 26 | 27 | public int getSeqNum() { 28 | return seqNum; 29 | } 30 | 31 | public int getWidth() { 32 | return width; 33 | } 34 | 35 | public int getHeight() { 36 | return height; 37 | } 38 | 39 | public int getxOff() { 40 | return xOff; 41 | } 42 | 43 | public int getyOff() { 44 | return yOff; 45 | } 46 | 47 | public int getDelayNum() { 48 | return delayNum; 49 | } 50 | 51 | public int getDelayDen() { 52 | return delayDen; 53 | } 54 | 55 | public byte getDisposeOp() { 56 | return disposeOp; 57 | } 58 | 59 | public byte getBlendOp() { 60 | return blendOp; 61 | } 62 | 63 | @Override 64 | protected void parseData(ApngDataSupplier data) { 65 | this.seqNum = data.readInt(); 66 | this.width = data.readInt(); 67 | this.height = data.readInt(); 68 | this.xOff = data.readInt(); 69 | this.yOff = data.readInt(); 70 | this.delayNum = data.readShort(); 71 | this.delayDen = data.readShort(); 72 | this.disposeOp = data.readByte(); 73 | this.blendOp = data.readByte(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngFrame.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import java.io.*; 4 | 5 | /** 6 | * Apng Frame Data 7 | * 8 | * @author ltf 9 | * @since 16/11/28, 下午1:15 10 | */ 11 | public class ApngFrame extends ApngFCTLChunk { 12 | 13 | InputStream imageStream; 14 | 15 | public InputStream getImageStream() { 16 | return imageStream; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngFrameRender.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import android.graphics.*; 4 | 5 | import static com.apng.ApngFCTLChunk.*; 6 | 7 | /** 8 | * 帧图像合成器 9 | * 10 | * @author ltf 11 | * @since 16/12/2, 上午9:10 12 | */ 13 | public class ApngFrameRender { 14 | private Rect mFullRect = new Rect(); 15 | 16 | private Bitmap mRenderFrame; 17 | private Canvas mRenderCanvas; 18 | 19 | private Bitmap mDisposedFrame; 20 | private Canvas mDisposeCanvas; 21 | private Rect mDisposeRect = new Rect(); 22 | private byte mLastDisposeOp = APNG_DISPOSE_OP_NONE; 23 | 24 | /** 25 | * 渲染当前帧画面 26 | * 27 | * @param frame apng中当前帧 28 | * @return 渲染合成后的当前帧图像 29 | */ 30 | public Bitmap render(ApngFrame frame, Bitmap frameBmp) { 31 | // 执行消除操作 32 | dispose(frame); 33 | // 合成当前帧 34 | blend(frame, frameBmp); 35 | return mRenderFrame; 36 | } 37 | 38 | /** 39 | * 首次使用或改变宽高时,要先调用本方法进行初始化 40 | */ 41 | public void prepare(int width, int height) { 42 | if (mRenderFrame == null || mFullRect.width() != width || mFullRect.height() != height) { 43 | // recycle previous allocated resources 44 | recycle(); 45 | // create new size cache 46 | mRenderFrame = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 47 | mDisposedFrame = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 48 | mFullRect.set(0, 0, width, height); 49 | if (mRenderCanvas == null) { 50 | mRenderCanvas = new Canvas(mRenderFrame); 51 | mDisposeCanvas = new Canvas(mDisposedFrame); 52 | } else { 53 | mRenderCanvas.setBitmap(mRenderFrame); 54 | mDisposeCanvas.setBitmap(mDisposedFrame); 55 | } 56 | } 57 | mDisposeRect.set(0, 0, width, height); 58 | mLastDisposeOp = APNG_DISPOSE_OP_BACKGROUND; 59 | } 60 | 61 | /** 62 | * 不再使用时,回收资源 63 | */ 64 | public void recycle() { 65 | //if (mRenderFrame != null) { 66 | // mRenderFrame.recycle(); 67 | // mDisposedFrame.recycle(); 68 | //} 69 | } 70 | 71 | /** 72 | * 帧图像析构消除 - 提交结果 73 | */ 74 | private void dispose(ApngFrame frame) { 75 | // last frame dispose op 76 | switch (mLastDisposeOp) { 77 | case APNG_DISPOSE_OP_NONE: 78 | // no op 79 | break; 80 | 81 | case APNG_DISPOSE_OP_BACKGROUND: 82 | // clear rect 83 | mRenderCanvas.clipRect(mDisposeRect); 84 | mRenderCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 85 | mRenderCanvas.clipRect(mFullRect, Region.Op.REPLACE); 86 | break; 87 | 88 | case APNG_DISPOSE_OP_PREVIOUS: 89 | // swap work and cache bitmap 90 | Bitmap bmp = mRenderFrame; 91 | mRenderFrame = mDisposedFrame; 92 | mDisposedFrame = bmp; 93 | mRenderCanvas.setBitmap(mRenderFrame); 94 | mDisposeCanvas.setBitmap(mDisposedFrame); 95 | break; 96 | } 97 | 98 | // current frame dispose op 99 | mLastDisposeOp = frame.getDisposeOp(); 100 | switch (mLastDisposeOp) { 101 | case APNG_DISPOSE_OP_NONE: 102 | // no op 103 | break; 104 | 105 | case APNG_DISPOSE_OP_BACKGROUND: 106 | // cache rect for next clear dispose 107 | int x = frame.getxOff(); 108 | int y = frame.getyOff(); 109 | mDisposeRect.set(x, y, x + frame.getWidth(), y + frame.getHeight()); 110 | break; 111 | 112 | case APNG_DISPOSE_OP_PREVIOUS: 113 | // cache bmp for next restore dispose 114 | mDisposeCanvas.clipRect(mFullRect, Region.Op.REPLACE); 115 | mDisposeCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 116 | mDisposeCanvas.drawBitmap(mRenderFrame, 0, 0, null); 117 | break; 118 | } 119 | } 120 | 121 | /** 122 | * 帧图像合成 123 | */ 124 | private void blend(ApngFrame frame, Bitmap frameBmp) { 125 | int xOff = frame.getxOff(); 126 | int yOff = frame.getyOff(); 127 | 128 | mRenderCanvas.clipRect(xOff, yOff, xOff + frame.getWidth(), yOff + frame.getHeight()); 129 | if (frame.getBlendOp() == APNG_BLEND_OP_SOURCE) { 130 | mRenderCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 131 | } 132 | mRenderCanvas.drawBitmap(frameBmp, xOff, yOff, null); 133 | mRenderCanvas.clipRect(mFullRect, Region.Op.REPLACE); 134 | 135 | 136 | } 137 | 138 | 139 | } 140 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngIHDRChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * IDHR Chunk 5 | * 6 | * @author ltf 7 | * @since 16/11/30, 下午4:45 8 | */ 9 | public class ApngIHDRChunk extends ApngDataChunk { 10 | private int width; 11 | private int height; 12 | private int bitDepth; 13 | private int colorType; 14 | private int compressMethod; 15 | private int filterMethod; 16 | private int interlaceMethod; 17 | 18 | public int getWidth() { 19 | return width; 20 | } 21 | 22 | public int getHeight() { 23 | return height; 24 | } 25 | 26 | public int getBitDepth() { 27 | return bitDepth; 28 | } 29 | 30 | public int getColorType() { 31 | return colorType; 32 | } 33 | 34 | public int getCompressMethod() { 35 | return compressMethod; 36 | } 37 | 38 | public int getFilterMethod() { 39 | return filterMethod; 40 | } 41 | 42 | public int getInterlaceMethod() { 43 | return interlaceMethod; 44 | } 45 | 46 | @Override 47 | protected void parseData(ApngDataSupplier data) { 48 | this.width = data.readInt(); 49 | this.height = data.readInt(); 50 | this.bitDepth = data.readByte(); 51 | this.colorType = data.readByte(); 52 | this.compressMethod = data.readByte(); 53 | this.filterMethod = data.readByte(); 54 | this.interlaceMethod = data.readByte(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngMmapParserChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import java.io.*; 4 | import java.nio.*; 5 | 6 | /** 7 | * Parsable Apng Chunk Over MappedByteBuffer 8 | * 9 | * @author ltf 10 | * @since 16/11/26, 下午12:11 11 | */ 12 | public class ApngMmapParserChunk extends ApngPaserChunk { 13 | // data buffer 14 | protected final MappedByteBuffer mBuf; 15 | 16 | // used to store the read pointer when Read Chunk As Stream 17 | // when lastPos>=0, it's locked, else if lastPost=-1, it's not locked 18 | private int lastPos = -1; 19 | 20 | public ApngMmapParserChunk(MappedByteBuffer mBuf) { 21 | this.mBuf = mBuf; 22 | } 23 | 24 | ApngMmapParserChunk(ApngMmapParserChunk copyFromChunk) { 25 | super(copyFromChunk); 26 | this.mBuf = copyFromChunk.mBuf; 27 | lastPos = copyFromChunk.lastPos; 28 | } 29 | 30 | @Override 31 | public void parsePrepare(int offset) { 32 | super.parsePrepare(offset); 33 | mBuf.position(offset); 34 | lastPos = -1; // parse prepare will clear readLock 35 | } 36 | 37 | @Override 38 | public int readInt() { 39 | return mBuf.getInt(); 40 | } 41 | 42 | @Override 43 | public short readShort() { 44 | return mBuf.getShort(); 45 | } 46 | 47 | @Override 48 | public byte readByte() { 49 | return mBuf.get(); 50 | } 51 | 52 | @Override 53 | public void move(int distance) { 54 | mBuf.position(mBuf.position() + distance); 55 | } 56 | 57 | /** 58 | * the total chunk as a stream's length, that's size+code+data+crc all sections' length 59 | */ 60 | int getStreamLen() { 61 | return length + 12; 62 | } 63 | 64 | /** 65 | * save current read pointer, and reset it to data section's head for read 66 | */ 67 | void lockRead() { 68 | lastPos = mBuf.position(); 69 | mBuf.position(offset); 70 | } 71 | 72 | /** 73 | * save current read pointer, and set it to specified startOffset for read 74 | */ 75 | void lockRead(int startOffset) { 76 | lastPos = mBuf.position(); 77 | mBuf.position(startOffset); 78 | } 79 | 80 | /** 81 | * restore read pointer lastPosition before call readAsStream() 82 | */ 83 | void unlockRead() { 84 | if (lastPos >= 0) { 85 | mBuf.position(lastPos); 86 | lastPos = -1; 87 | } 88 | } 89 | 90 | /** 91 | * read data to buffer array 92 | *

93 | * !!! ATTENTION: must call lockRead() to move read pointer to head before call this function 94 | * 95 | * @param buffer target buffer array 96 | * @param byteOffset offset at target buffer array 97 | * @param byteCount bytes to read 98 | * @return readed bytes 99 | * @throws IOException 100 | */ 101 | int readAsStream(byte[] buffer, int byteOffset, int byteCount) throws IOException { 102 | int size = nextOffset - mBuf.position(); 103 | if (size <= 0) return 0; 104 | size = size > byteCount ? byteCount : size; 105 | 106 | mBuf.get(buffer, byteOffset, size); 107 | return size; 108 | } 109 | 110 | /** 111 | * assign to a data chunk to holder the data 112 | */ 113 | void assignTo(ApngDataChunk dataChunk) { 114 | int pos = mBuf.position(); 115 | mBuf.position(offset); 116 | try { 117 | dataChunk.parse(this); 118 | } finally { 119 | mBuf.position(pos); 120 | } 121 | } 122 | 123 | /** 124 | * duplicate this chunk's data to an array 125 | */ 126 | public byte[] duplicateData() throws IOException { 127 | byte[] data = new byte[getStreamLen()]; 128 | lockRead(); 129 | readAsStream(data, 0, data.length); 130 | unlockRead(); 131 | return data; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngPaserChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import static com.apng.ApngConst.*; 4 | 5 | /** 6 | * Apng Chunk Parser 7 | * 8 | * @author ltf 9 | * @since 16/11/26, 下午12:09 10 | */ 11 | abstract class ApngPaserChunk extends ApngChunk implements ApngDataSupplier { 12 | // chunk start offset, SHOULD NOT CHANGE AFTER PARSE PREPARED 13 | // used for parse and read 14 | protected int offset; 15 | 16 | // next chunk start offset, INITED AFTER CALL parse(), used for parseNext 17 | protected int nextOffset; 18 | 19 | ApngPaserChunk() { 20 | } 21 | 22 | ApngPaserChunk(ApngPaserChunk copyFrom) { 23 | super(copyFrom); 24 | this.offset = copyFrom.offset; 25 | this.nextOffset = copyFrom.nextOffset; 26 | } 27 | 28 | int getOffset() { 29 | return offset; 30 | } 31 | 32 | /** 33 | * set the offset before parse 34 | * !!! ATTENTION !!! parseNext() will start parse from current prepared offset 35 | */ 36 | public void parsePrepare(int offset) { 37 | this.offset = offset; 38 | this.nextOffset = offset; 39 | } 40 | 41 | /** 42 | * parse chunk info, 43 | * and return next chunk's start position, or return -1 if this is the last chunk 44 | * ATTENTION: must call parsePrepare() to init the offset before call this function 45 | */ 46 | public int parse() { 47 | length = readInt(); 48 | typeCode = readInt(); 49 | parseData(); 50 | this.crc = readInt(); 51 | nextOffset = typeCode == CODE_IEND ? -1 : offset + length + 12; 52 | return nextOffset; 53 | } 54 | 55 | /** 56 | * parse data 57 | * current read pointer is at the data start position, 58 | * after this function, should move read pointer to CRC's start position 59 | */ 60 | protected void parseData() { 61 | move(length); 62 | } 63 | 64 | /** 65 | * relocate current chunk to next, and parse the chunk info, 66 | * return next chunk(the one after current parsed chunk)'s start position, 67 | * or return -1 if this is the last chunk 68 | * !!! ATTENTION !!! 69 | * when parsePrepare() called, parseNext() = parse(), 70 | * it will start parse from current prepared offset 71 | */ 72 | public int parseNext() { 73 | parsePrepare(nextOffset); 74 | return parse(); 75 | } 76 | 77 | /** 78 | * parse info from next chunk, and locate for the specified typeCode chunk 79 | * and return next chunk's start position, or return -1 if this is the last chunk 80 | */ 81 | int locateNext(int chunkTypeCode) { 82 | parseNext(); 83 | while (typeCode != chunkTypeCode && nextOffset > 0) { 84 | parseNext(); 85 | } 86 | return nextOffset; 87 | } 88 | 89 | /** 90 | * read int from data and move the pointer 4 byte ahead 91 | */ 92 | abstract public int readInt(); 93 | 94 | /** 95 | * read int from data and move the pointer 2 byte ahead 96 | */ 97 | abstract public short readShort(); 98 | 99 | /** 100 | * read int from data and move the pointer 1 byte ahead 101 | */ 102 | abstract public byte readByte(); 103 | 104 | /** 105 | * move the pointer ahead by distance bytes 106 | */ 107 | abstract public void move(int distance); 108 | } 109 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ApngReader.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import java.io.*; 4 | import java.nio.*; 5 | import java.nio.channels.*; 6 | import java.util.*; 7 | 8 | import static com.apng.ApngConst.*; 9 | 10 | /** 11 | * Apng加载器(从Apng文件中读取每一帧的控制块及图像) 12 | * 13 | * @author ltf 14 | * @since 16/11/25, 上午8:14 15 | */ 16 | public class ApngReader { 17 | 18 | /** 19 | * chunks should be copied to each frame 20 | */ 21 | public static final int[] COPIED_TYPE_CODES = { 22 | CODE_iCCP, 23 | CODE_sRGB, 24 | CODE_sBIT, 25 | CODE_gAMA, 26 | CODE_cHRM, 27 | 28 | CODE_PLTE, 29 | 30 | CODE_tRNS, 31 | CODE_hIST, 32 | CODE_bKGD, 33 | CODE_pHYs, 34 | CODE_sPLT 35 | }; 36 | 37 | static { 38 | Arrays.sort(COPIED_TYPE_CODES); 39 | } 40 | 41 | private final MappedByteBuffer mBuffer; 42 | private final ApngMmapParserChunk mChunk; 43 | private final PngStream mPngStream = new PngStream(); 44 | private ApngACTLChunk mActlChunk; 45 | 46 | public ApngReader(String apngFile) throws IOException, FormatNotSupportException { 47 | RandomAccessFile f = new RandomAccessFile(apngFile, "r"); 48 | mBuffer = f.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, f.length()); 49 | f.close(); 50 | if (mBuffer.getInt() != PNG_SIG 51 | && mBuffer.getInt(4) != PNG_SIG_VER 52 | && mBuffer.getInt(8) != CODE_IHDR) { 53 | throw new FormatNotSupportException("Not a png/apng file"); 54 | } 55 | mChunk = new ApngMmapParserChunk(mBuffer); 56 | reset(); 57 | } 58 | 59 | /** 60 | * get the acTL chunk information 61 | * 62 | * @return animation control info 63 | * @throws IOException 64 | * @throws FormatNotSupportException 65 | */ 66 | public ApngACTLChunk getACTL() throws IOException, FormatNotSupportException { 67 | if (mActlChunk != null) return mActlChunk; 68 | int pos = mBuffer.position(); 69 | try { 70 | ApngMmapParserChunk tmpChunk = new ApngMmapParserChunk(mBuffer); 71 | // locate first chunk (IHDR) 72 | tmpChunk.parsePrepare(8); 73 | tmpChunk.parse(); 74 | 75 | // locate ACTL chunk 76 | while (tmpChunk.typeCode != CODE_acTL) { 77 | if (tmpChunk.typeCode == CODE_IEND || tmpChunk.parseNext() < 0) { 78 | throw new FormatNotSupportException("No ACTL chunk founded, not an apng file. (maybe it's a png only)"); 79 | } 80 | } 81 | 82 | handleACTL(tmpChunk); 83 | } finally { 84 | mBuffer.position(pos); 85 | } 86 | return mActlChunk; 87 | } 88 | 89 | /** 90 | * hanlde actl chunk 91 | */ 92 | private void handleACTL(ApngMmapParserChunk chunk) throws IOException { 93 | if (mActlChunk == null) { 94 | mActlChunk = new ApngACTLChunk(); 95 | chunk.assignTo(mActlChunk); 96 | } 97 | } 98 | 99 | /** 100 | * handle other's chunk 101 | */ 102 | private void handleOtherChunk(ApngMmapParserChunk chunk) throws IOException { 103 | if (Arrays.binarySearch(COPIED_TYPE_CODES, chunk.typeCode) >= 0) { 104 | mPngStream.setHeadData(chunk.getTypeCode(), chunk.duplicateData()); 105 | } 106 | } 107 | 108 | /** 109 | * get next frame control info & bitmap 110 | * 111 | * @return next frame control info, or null if no next FCTL chunk || no next IDAT/FDAT 112 | * @throws IOException 113 | */ 114 | public ApngFrame nextFrame() throws IOException { 115 | // reset read pointers from previous frame's lock 116 | mPngStream.clearDataChunks(); 117 | mPngStream.resetPos(); 118 | mChunk.unlockRead(); 119 | 120 | // locate next FCTL chunk 121 | boolean ihdrCopied = false; 122 | while (mChunk.typeCode != CODE_fcTL) { 123 | switch (mChunk.typeCode) { 124 | case CODE_IEND: 125 | return null; 126 | case CODE_IHDR: 127 | mPngStream.setIHDR(mChunk.duplicateData()); 128 | break; 129 | case CODE_acTL: 130 | handleACTL(mChunk); 131 | ihdrCopied = true; 132 | break; 133 | default: 134 | handleOtherChunk(mChunk); 135 | } 136 | mChunk.parseNext(); 137 | } 138 | 139 | // located at FCTL chunk 140 | ApngFrame frame = new ApngFrame(); 141 | mChunk.assignTo(frame); 142 | 143 | // locate next IDAT or fdAt chunk 144 | mChunk.parseNext();// first move next from current FCTL 145 | while (mChunk.typeCode != CODE_IDAT && mChunk.typeCode != CODE_fdAT) { 146 | switch (mChunk.typeCode) { 147 | case CODE_IEND: 148 | return null; 149 | case CODE_IHDR: 150 | mPngStream.setIHDR(mChunk.duplicateData()); 151 | ihdrCopied = true; 152 | break; 153 | case CODE_acTL: 154 | handleACTL(mChunk); 155 | break; 156 | default: 157 | handleOtherChunk(mChunk); 158 | } 159 | mChunk.parseNext(); 160 | } 161 | 162 | // located at first IDAT or fdAT chunk 163 | // collect all consecutive dat chunks 164 | boolean needUpdateIHDR = true; 165 | int dataOffset = mChunk.getOffset(); 166 | while (mChunk.typeCode == CODE_fdAT || mChunk.typeCode == CODE_IDAT) { 167 | if (needUpdateIHDR && (!ihdrCopied || mChunk.typeCode == CODE_fdAT)) { 168 | mPngStream.updateIHDR(frame.getWidth(), frame.getHeight()); 169 | needUpdateIHDR = false; 170 | } 171 | 172 | if (mChunk.typeCode == CODE_fdAT) { 173 | mPngStream.addDataChunk(new Fdat2IdatChunk(mChunk)); 174 | } else { 175 | mPngStream.addDataChunk(new ApngMmapParserChunk(mChunk)); 176 | } 177 | mChunk.parseNext(); 178 | } 179 | 180 | // lock position for this frame's image as OutputStream 181 | mChunk.lockRead(dataOffset); 182 | frame.imageStream = mPngStream; 183 | return frame; 184 | } 185 | 186 | /** 187 | * locate to the first chunk, and parse it 188 | */ 189 | public void reset() { 190 | mChunk.parsePrepare(8); 191 | mChunk.parse(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/ByteUtil.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import java.io.*; 4 | 5 | /** 6 | * Created by Shark0 on 2016/9/13. 7 | */ 8 | public class ByteUtil { 9 | public static int indexOf(byte[] bytes1, byte[] bytes2) { 10 | for (int i = 0; i < bytes1.length - bytes2.length + 1; i++) { 11 | boolean found = true; 12 | for (int j = 0; j < bytes2.length; j++) { 13 | if (bytes1[i + j] != bytes2[j]) { 14 | found = false; 15 | break; 16 | } 17 | } 18 | if (found) return i; 19 | } 20 | return -1; 21 | } 22 | 23 | public static byte[] subBytes(byte[] bytes, int startIndex, int endIndex) { 24 | byte[] subBytes = new byte[endIndex - startIndex]; 25 | for(int i = startIndex; i < endIndex; i ++) { 26 | subBytes[i - startIndex] = bytes[i]; 27 | } 28 | return subBytes; 29 | } 30 | 31 | public static String bytesToHex(byte[] bytes) { 32 | StringBuilder stringBuilder = new StringBuilder(bytes.length * 2); 33 | for(byte b: bytes) { 34 | String hex = String.format("%02x", b & 0xff); 35 | stringBuilder.append(hex); 36 | } 37 | return stringBuilder.toString(); 38 | } 39 | 40 | public static int bytesToInt(byte[] bytes) { 41 | return bytes[0] << 24 | (bytes[1] & 0xFF) << 16 | (bytes[2] & 0xFF) << 8 | (bytes[3] & 0xFF); 42 | } 43 | 44 | public static byte[] intToBytes(int value) { 45 | return new byte[] { 46 | (byte)(value >> 24), 47 | (byte)(value >> 16), 48 | (byte)(value >> 8), 49 | (byte)value }; 50 | } 51 | 52 | public static int bytesToshort(byte[] bytes) { 53 | return bytes[0] << 8 | (bytes[1] & 0xFF); 54 | } 55 | 56 | public static byte[] shortToBytes(int value) { 57 | return new byte[] { 58 | (byte)(value >> 8), 59 | (byte)value }; 60 | } 61 | 62 | public static byte[] combineBytes(byte[] bytes1, byte[] bytes2) { 63 | byte[] bytes = new byte[bytes1.length + bytes2.length]; 64 | for(int i = 0; i < bytes1.length; i ++) { 65 | bytes[i] = bytes1[i]; 66 | } 67 | for(int i = 0; i < bytes2.length; i ++) { 68 | bytes[i + bytes1.length] = bytes2[i]; 69 | } 70 | return bytes; 71 | } 72 | 73 | public static byte[] copyBytes(byte[] bytes) { 74 | byte[] newBytes = new byte[bytes.length]; 75 | for(int i = 0; i < bytes.length; i ++) { 76 | newBytes[i] = bytes[i]; 77 | } 78 | return newBytes; 79 | } 80 | 81 | 82 | public static byte[] loadBytesFromFile(File file) { 83 | BufferedInputStream inputStream = null; 84 | int size = (int) file.length(); 85 | byte[] imageBytes = new byte[size]; 86 | try { 87 | inputStream = new BufferedInputStream(new FileInputStream(file)); 88 | inputStream.read(imageBytes, 0, imageBytes.length); 89 | inputStream.close(); 90 | } catch (IOException e) { 91 | e.printStackTrace(); 92 | return null; 93 | } finally { 94 | if (inputStream != null) { 95 | try { 96 | inputStream.close(); 97 | } catch (Exception e) { 98 | e.printStackTrace(); 99 | } 100 | } 101 | } 102 | return imageBytes; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/Fdat2IdatChunk.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import java.io.*; 4 | import java.util.zip.*; 5 | 6 | import static com.apng.PngStream.*; 7 | 8 | /** 9 | * convert fdAT chunk to IDAT chunk stream 10 | * 11 | * @author ltf 12 | * @since 16/12/2, 下午3:56 13 | */ 14 | public class Fdat2IdatChunk extends ApngMmapParserChunk { 15 | private int mDataSigOff; // signature "fdAT" 's offset 16 | private int mDataSigdEnd; // d's end(or A's position), in signature "fdAT" 17 | private int mDataCrcOff; // data CRC's offset 18 | private int mFDATSeqOff; // offset of "fdAT"'s sequence_number, only available when mIsFDAT = true 19 | private int mFDATSeqEnd; // end of "fdAT"'s sequence_number, only available when mIsFDAT = true 20 | private CRC32 mCrc = new CRC32(); 21 | private boolean mCalCrc = true; // need to compute/calculate CRC 22 | private byte[] mCrcVal = new byte[4]; // used for fdAT recompute crc 23 | private byte[] mFDATLength = new byte[4];// used for fdAT recompute length 24 | 25 | Fdat2IdatChunk(ApngMmapParserChunk copyFromChunk) { 26 | super(copyFromChunk); 27 | init(); 28 | } 29 | 30 | private void init() { 31 | mDataSigOff = offset + 4; 32 | mDataSigdEnd = offset + 6; 33 | mFDATSeqOff = offset + 8; 34 | mFDATSeqEnd = offset + 12; 35 | mDataCrcOff = nextOffset - 4; 36 | intToArray(length - 4, mFDATLength, 0); 37 | } 38 | 39 | @Override 40 | int getStreamLen() { 41 | return length + 8; // FDAT covert to IDAT will lost it's 4byte seq_num 42 | } 43 | 44 | // this function is optimized for performance, so it's maybe hard to read and control 45 | @Override 46 | int readAsStream(byte[] buffer, int byteOffset, int byteCount) throws IOException { 47 | int pos = mBuf.position(); 48 | int size = nextOffset - pos; 49 | if (pos < mFDATSeqEnd) { 50 | int removed = mFDATSeqEnd - pos; 51 | size -= removed > 4 ? 4 : removed; 52 | } 53 | if (size <= 0) return 0; 54 | size = size > byteCount ? byteCount : size; 55 | int dstEndOffset = byteOffset + size; 56 | 57 | for (int want = size; want > 0; ) { 58 | int count; 59 | if (pos >= mDataCrcOff) { 60 | // read DATA CRC 61 | count = nextOffset - pos; 62 | count = want < count ? want : count; 63 | 64 | // CRC only calculated for one times 65 | if (mCalCrc) { 66 | intToArray((int) mCrc.getValue(), mCrcVal, 0); 67 | mCalCrc = false; 68 | } 69 | System.arraycopy(mCrcVal, 4 - (nextOffset - pos), buffer, dstEndOffset - want, count); 70 | move(count); 71 | } else if (pos >= mFDATSeqEnd) { 72 | // all raw data don't need modify 73 | count = mDataCrcOff - pos; 74 | count = want < count ? want : count; 75 | mBuf.get(buffer, dstEndOffset - want, count); 76 | // compute crc for fdAT 77 | if (mCalCrc) mCrc.update(buffer, dstEndOffset - want, count); 78 | //Log.d("ApngSurfaceView", String.format("r: %d, crc: %d", read - pre, System.currentTimeMillis() - read)); 79 | } else if (pos >= mFDATSeqOff) { 80 | // seq_num chunk will be skipped 81 | count = mFDATSeqEnd - pos; 82 | count = want < count ? want : count; 83 | want += count; 84 | move(count); 85 | } else { 86 | // data trunk header( length + type_code) 87 | count = mFDATSeqOff - pos; 88 | count = want < count ? want : count; 89 | mBuf.get(buffer, dstEndOffset - want, count); 90 | 91 | // update fdAT to IDAT 92 | if (pos < mDataSigdEnd) { 93 | int dOff = mDataSigdEnd - pos - 2; 94 | int cover = count - dOff; 95 | if (cover >= 2) { 96 | if (count > 1) buffer[dstEndOffset - want + dOff] = 'I'; 97 | buffer[dstEndOffset - want + dOff + 1] = 'D'; 98 | } else if (cover == 1) { 99 | buffer[dstEndOffset - want + dOff] = 'I'; 100 | } 101 | } 102 | 103 | // calculate CRC 104 | if (pos >= mDataSigOff) { 105 | if (mCalCrc) mCrc.update(buffer, dstEndOffset - want, count); 106 | } else { 107 | // compute CRC on bytes included 108 | int dOff = mDataSigOff - pos; 109 | int cover = count - dOff; 110 | if (mCalCrc && cover > 0) { 111 | mCrc.update(buffer, dstEndOffset - want + dOff, cover); 112 | } 113 | 114 | // update length 115 | cover = dOff < count ? dOff : count; 116 | if (cover > 0) System.arraycopy(mFDATLength, 4 - dOff, buffer, dstEndOffset - want, cover); 117 | } 118 | } 119 | want -= count; 120 | pos += count; 121 | } 122 | return size; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/FormatNotSupportException.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | /** 4 | * Not Support Apng File Format Exception 5 | * 6 | * @author ltf 7 | * @since 16/11/26, 下午4:16 8 | */ 9 | public class FormatNotSupportException extends Exception { 10 | 11 | public FormatNotSupportException(String detailMessage) { 12 | super(detailMessage); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/PngStream.java: -------------------------------------------------------------------------------- 1 | package com.apng; 2 | 3 | import java.io.*; 4 | import java.util.*; 5 | import java.util.zip.*; 6 | 7 | /** 8 | * Png Stream Constructor to make png stream from apng frame 9 | * 10 | * @author ltf 11 | * @since 16/11/28, 上午8:20 12 | */ 13 | public class PngStream extends InputStream { 14 | // fast data 15 | public static final byte[] PNG_SIG_DAT = {-119, 80, 78, 71, 13, 10, 26, 10}; 16 | public static final int PNG_SIG_LEN = PNG_SIG_DAT.length; 17 | public static final byte[] PNG_IEND_DAT = {0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; 18 | public static final int PNG_IEND_DAT_LEN = PNG_IEND_DAT.length; 19 | public static final byte[] NODATA = {}; 20 | 21 | public static final int IHDR_LEN = 25; 22 | public static final int IHDR_WIDTH_OFF = PNG_SIG_LEN + 8; // width's offset in IHDR 23 | public static final int IHDR_HEIGHT_OFF = IHDR_WIDTH_OFF + 4; 24 | public static final int IHDR_CRC_OFF = PNG_SIG_LEN + IHDR_LEN - 4; 25 | 26 | private byte[] mHeadData = new byte[PNG_SIG_LEN + IHDR_LEN]; // cached PNG_SIG_VER and IHDR and PLTE(optional) data 27 | private int mHeadDataLen = PNG_SIG_LEN + IHDR_LEN; 28 | /** 29 | * block infos chain for manage the head data 30 | */ 31 | private BlockInfo mBlockInfos; 32 | 33 | private int mIENDOffset; // IEND's offset, that's the length of all previous sections 34 | private int mPos = 0; 35 | private int mLen = 0; 36 | 37 | private CRC32 mCrc = new CRC32(); 38 | private ArrayList mDataChunks = new ArrayList<>(3); 39 | private int dataChunkIndex; 40 | 41 | public PngStream() { 42 | System.arraycopy(PNG_SIG_DAT, 0, mHeadData, 0, PNG_SIG_LEN); 43 | } 44 | 45 | /** 46 | * set IHDR data 47 | */ 48 | void setIHDR(byte[] ihdrData) { 49 | System.arraycopy(ihdrData, 0, mHeadData, PNG_SIG_LEN, IHDR_LEN); 50 | } 51 | 52 | /** 53 | * remove head Data by typeCode 54 | */ 55 | void removeHeadData(final int typeCode) { 56 | setHeadData(typeCode, NODATA); 57 | } 58 | 59 | /** 60 | * set(add/update) head Data by typeCode 61 | */ 62 | void setHeadData(final int typeCode, byte[] data) { 63 | BlockInfo block = getBlockInfo(typeCode, true); 64 | int delta = data.length - block.len; 65 | int oldHeadDataLen = mHeadDataLen; 66 | mHeadDataLen += delta; 67 | int oldNextOff = block.offset + block.len; 68 | 69 | byte[] src = mHeadData; 70 | // increase mHeadData size if needed 71 | if (delta > 0 && mHeadData.length < mHeadDataLen) { 72 | mHeadData = new byte[mHeadDataLen]; 73 | // only copy data before current chunk 74 | // others will be copied in next move all follows data operation 75 | System.arraycopy(src, 0, mHeadData, 0, block.offset); 76 | } 77 | 78 | // new data size are different, move back/ahead all follows data 79 | if (delta != 0) { 80 | System.arraycopy( 81 | src, oldNextOff, 82 | mHeadData, oldNextOff + delta, 83 | oldHeadDataLen - oldNextOff); 84 | updateNextOffsetTillEnd(block, delta); 85 | block.len = data.length; 86 | } 87 | 88 | System.arraycopy(data, 0, mHeadData, block.offset, data.length); 89 | 90 | /** 91 | * remove blockInfo if nodata contains 92 | */ 93 | if (data.length == 0) { 94 | if (block == mBlockInfos) { 95 | mBlockInfos = null; 96 | } else { 97 | BlockInfo pre = block.pre; 98 | pre.next = block.next; 99 | if (pre.next != null) pre.next.pre = pre; 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * update ALL FOLLOWS blocks' offset, NOT include current block's offset 106 | * 107 | * @param currentBlock current block !!!NonNull !!! 108 | * @param delta delta plus to current offset 109 | */ 110 | private void updateNextOffsetTillEnd(BlockInfo currentBlock, int delta) { 111 | BlockInfo block = currentBlock.next; 112 | while (block != null) { 113 | block.offset += delta; 114 | block = block.next; 115 | } 116 | } 117 | 118 | /** 119 | * locate blockInfo by typeCode 120 | * 121 | * @param typeCode typeCode 122 | * @param createIfNotExists create one if not exists 123 | * @return blockInfo, or null if not exists and not create new 124 | */ 125 | private BlockInfo getBlockInfo(final int typeCode, boolean createIfNotExists) { 126 | BlockInfo block = mBlockInfos; 127 | BlockInfo last = mBlockInfos; 128 | while (block != null) { 129 | if (block.typeCode == typeCode) { 130 | return block; 131 | } 132 | if (block.next == null) { 133 | last = block; 134 | } 135 | block = block.next; 136 | } 137 | 138 | if (createIfNotExists) { 139 | block = new BlockInfo(typeCode); 140 | if (last != null) { 141 | block.pre = last; 142 | last.next = block; 143 | block.offset = last.offset + last.len; 144 | } else { 145 | mBlockInfos = block; 146 | block.offset = PNG_SIG_LEN + IHDR_LEN; 147 | } 148 | return block; 149 | } 150 | return null; 151 | } 152 | 153 | /** 154 | * 3rd: set data chunk each time when use this to construct a frame png stream 155 | */ 156 | void addDataChunk(ApngMmapParserChunk dataChunk) { 157 | this.mDataChunks.add(dataChunk); 158 | mIENDOffset = mHeadDataLen; 159 | for (ApngMmapParserChunk chunk : mDataChunks) 160 | mIENDOffset += chunk.getStreamLen(); 161 | mLen = mIENDOffset + PNG_IEND_DAT_LEN; 162 | } 163 | 164 | /** 165 | * clear data trunks and reset dataChunkIndex, mPos 166 | */ 167 | void clearDataChunks() { 168 | mDataChunks.clear(); 169 | dataChunkIndex = 0; 170 | mPos = 0; 171 | } 172 | 173 | /** 174 | * update IHDR width and height and chunk crc 175 | */ 176 | void updateIHDR(int width, int height) { 177 | intToArray(width, mHeadData, IHDR_WIDTH_OFF); 178 | intToArray(height, mHeadData, IHDR_HEIGHT_OFF); 179 | mCrc.reset(); 180 | mCrc.update(mHeadData, IHDR_WIDTH_OFF - 4, IHDR_CRC_OFF - IHDR_WIDTH_OFF + 4); 181 | intToArray((int) mCrc.getValue(), mHeadData, IHDR_CRC_OFF); 182 | } 183 | 184 | /** 185 | * reset read position to head, dataChunkIndex to 0 186 | */ 187 | void resetPos() { 188 | dataChunkIndex = 0; 189 | mPos = 0; 190 | } 191 | 192 | @Override 193 | public int read() throws IOException { 194 | throw new UnsupportedOperationException("not support read by byte because of low performance"); 195 | } 196 | 197 | // this function is optimized for performance, so it's maybe hard to read and control 198 | @Override 199 | public int read(byte[] buffer, final int byteOffset, final int byteCount) throws IOException { 200 | int size = mLen - mPos; 201 | if (size <= 0) return 0; 202 | size = size > byteCount ? byteCount : size; 203 | int dstEndOffset = byteOffset + size; 204 | 205 | for (int want = size; want > 0; ) { 206 | int count; 207 | if (mPos < mHeadDataLen) { 208 | // read from head data section 209 | count = mHeadDataLen - mPos; 210 | count = want < count ? want : count; 211 | System.arraycopy(mHeadData, mPos, buffer, dstEndOffset - want, count); 212 | } else if (mPos >= mIENDOffset) { 213 | // read from IEND data section 214 | count = mLen - mPos; 215 | count = want < count ? want : count; 216 | System.arraycopy(PNG_IEND_DAT, mPos - mIENDOffset, buffer, dstEndOffset - want, count); 217 | } else { 218 | // data trunk header( length + type_code) 219 | count = mIENDOffset - mPos; 220 | count = want < count ? want : count; 221 | int readed = mDataChunks.get(dataChunkIndex).readAsStream(buffer, dstEndOffset - want, count); 222 | // switch read from next data chunk 223 | if (readed < count) { 224 | dataChunkIndex++; 225 | count = readed; 226 | } 227 | } 228 | want -= count; 229 | mPos += count; 230 | } 231 | return size; 232 | } 233 | 234 | /** 235 | * finally generate data crc value 236 | */ 237 | public static void intToArray(int val, byte[] arr, int offset) { 238 | arr[offset] = (byte) (val >> 24 & 0xFF); 239 | arr[offset + 1] = (byte) (val >> 16 & 0xFF); 240 | arr[offset + 2] = (byte) (val >> 8 & 0xFF); 241 | arr[offset + 3] = (byte) (val & 0xFF); 242 | } 243 | 244 | /** 245 | * Head data block info 246 | */ 247 | private static class BlockInfo { 248 | private int typeCode; 249 | int offset; 250 | int len; 251 | BlockInfo pre; 252 | BlockInfo next; 253 | 254 | public BlockInfo(int typeCode) { 255 | this.typeCode = typeCode; 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/entity/AnimParams.java: -------------------------------------------------------------------------------- 1 | package com.apng.entity; 2 | 3 | 4 | /** 5 | * @author xing.hu 6 | * @since 2016/5/24, 17:43 7 | */ 8 | public class AnimParams { 9 | 10 | /** 11 | * 等宽缩放 12 | */ 13 | public static final int WIDTH_SCALE_TYPE = 0x0001; 14 | /** 15 | * 等高缩放 16 | */ 17 | public static final int HEIGHT_SCALE_TYPE = 0x0010; 18 | /** 19 | * 按宽高比例较小的进行缩放 20 | */ 21 | public static final int WIDTH_OR_HEIGHT_SCALE_TYPE = 0x0100; 22 | 23 | 24 | /** 25 | * 缩放比例 26 | */ 27 | public int scaleType = WIDTH_SCALE_TYPE; 28 | 29 | 30 | /** 31 | * 一直循环播放 32 | */ 33 | public static int PLAY_4_LOOP = -1; 34 | 35 | /** 36 | * 对齐方式 37 | */ 38 | public int align = 1; 39 | 40 | /** 41 | * 对齐百分比 42 | */ 43 | public float percent = -1; 44 | 45 | /** 46 | *礼物展示权重 47 | */ 48 | public int weight = 0; 49 | 50 | public boolean isHasBackground = false; 51 | 52 | /** 53 | * 动效的名字 54 | */ 55 | public String name = ""; 56 | 57 | /** 58 | * 礼物动画本地路径 59 | */ 60 | public String imagePath; 61 | 62 | 63 | /** 64 | * 动画执行次数 65 | */ 66 | public int loopCount = 1; 67 | 68 | 69 | 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/utils/ApngDownloadUtil.java: -------------------------------------------------------------------------------- 1 | package com.apng.utils; 2 | 3 | 4 | import android.content.*; 5 | import android.os.*; 6 | import android.text.*; 7 | import android.util.*; 8 | 9 | import java.io.*; 10 | 11 | /** 12 | * @author xing.hu 13 | * @since 2016/04/11, 14:25 14 | * 下载礼物动效apng类 15 | */ 16 | public class ApngDownloadUtil { 17 | 18 | private static final String TAG = "ApngDownloadUtil"; 19 | 20 | 21 | /** 22 | * 获取Apng解压后的文件路径 23 | * @return 24 | */ 25 | public static File getWorkingExactDir() { 26 | File workingDir = null; 27 | String apngCachePath = ("/sdcard/apng/.nomedia/exact"); 28 | if (!TextUtils.isEmpty(apngCachePath)) { 29 | workingDir = new File(apngCachePath); 30 | if (!workingDir.exists()) { 31 | workingDir.mkdirs(); 32 | } 33 | } 34 | return workingDir; 35 | } 36 | 37 | public static String getFileCachePath(String uri, Context context) { 38 | // 只有在存在sdcard时才下载Apng动画 39 | if (!haveExterStorage()) {// 没有sdcard 40 | return null; 41 | } 42 | String apngPath = getFileDirs("apng/.nomedia/", context); 43 | if (apngPath != null) { 44 | File file = new File(apngPath, String.format("%s.png", Md5.toMD5(uri))); 45 | return file.getAbsolutePath(); 46 | } 47 | 48 | return null; 49 | } 50 | 51 | 52 | public static String getFileDirs(String dirName, Context context) { 53 | File externalCache = context.getExternalFilesDir(null); 54 | if (externalCache != null) { 55 | File cacheImg = new File(externalCache, dirName); 56 | if (!cacheImg.exists()) { 57 | cacheImg.mkdirs(); 58 | } 59 | 60 | if (cacheImg.canRead() && cacheImg.canWrite()) { 61 | return cacheImg.getAbsolutePath(); 62 | } 63 | 64 | } 65 | 66 | File innerCache = context.getFilesDir(); 67 | if (innerCache != null) { 68 | File cacheImg = new File(innerCache, dirName); 69 | if (!cacheImg.exists()) { 70 | cacheImg.mkdirs(); 71 | } 72 | 73 | if (cacheImg.canRead() && cacheImg.canWrite()) { 74 | return cacheImg.getAbsolutePath(); 75 | } 76 | } 77 | 78 | // 如果files目录获取不到, 则获取cache目录 79 | return getCacheDirs(dirName, context); 80 | } 81 | 82 | public static String getCacheDirs(String dirName, Context context) { 83 | File externalCache =context.getExternalCacheDir(); 84 | if (externalCache != null) { 85 | File cacheImg = new File(externalCache, dirName); 86 | if (!cacheImg.exists()) { 87 | cacheImg.mkdirs(); 88 | } 89 | 90 | if (cacheImg.canRead() && cacheImg.canWrite()) { 91 | return cacheImg.getAbsolutePath(); 92 | } 93 | 94 | } 95 | 96 | File innerCache = context.getCacheDir(); 97 | if (innerCache != null) { 98 | File cacheImg = new File(innerCache, dirName); 99 | if (!cacheImg.exists()) { 100 | cacheImg.mkdirs(); 101 | } 102 | 103 | if (cacheImg.canRead() && cacheImg.canWrite()) { 104 | return cacheImg.getAbsolutePath(); 105 | } 106 | } 107 | return null; 108 | } 109 | 110 | public static boolean haveExterStorage() { 111 | 112 | if (true) { 113 | return isAvaiableSpace(200); 114 | } 115 | String state = Environment.getExternalStorageState(); 116 | if (!Environment.MEDIA_MOUNTED.equals(state)) { 117 | return false; 118 | } 119 | // File sdcard = android.os.Environment.getExternalStorageDirectory(); 120 | // if (sdcard == null || !sdcard.exists()) { 121 | // return false; 122 | // } 123 | return true; 124 | } 125 | 126 | public static boolean isAvaiableSpace(int sizekb) { 127 | boolean ishasSpace = false; 128 | try { 129 | if (Environment.getExternalStorageState().equals( 130 | Environment.MEDIA_MOUNTED)) { 131 | String sdcard = Environment.getExternalStorageDirectory().getPath(); 132 | StatFs statFs = new StatFs(sdcard); 133 | long blockSize = statFs.getBlockSize(); 134 | long blocks = statFs.getAvailableBlocks(); 135 | long availableSpare = (blocks * blockSize) / (1024); 136 | Log.d("剩余空间", "availableSpare = " + availableSpare); 137 | if (availableSpare > sizekb) { 138 | ishasSpace = true; 139 | } 140 | } 141 | }catch (IllegalArgumentException ex){ 142 | ex.printStackTrace(); 143 | } 144 | 145 | return ishasSpace; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/utils/ApngUtils.java: -------------------------------------------------------------------------------- 1 | package com.apng.utils; 2 | 3 | import android.graphics.*; 4 | 5 | /** 6 | * @author xing.hu 7 | * @since 2016/11/9, 下午2:49 8 | * Apng动画播放工具类 9 | */ 10 | public class ApngUtils { 11 | public static final String TAG = "ApngUtils"; 12 | //上对齐 13 | public static final int APNG_ANIM_ALIGN_TOP = 1; 14 | //居中对齐 15 | public static final int APNG_ANIM_ALIGN_MIDDLE = 2; 16 | //下对齐 17 | public static final int APNG_ANIM_ALIGN_BOTTOM = 3; 18 | /** 19 | * 获取所绘制bitmap距离左边和顶部的距离 20 | * @param canvas:画布 21 | * @param bitmap:图片 22 | * @param align:上对齐、中对齐、下对齐 23 | * @return 24 | */ 25 | public static float[] getLeftAndTop(Canvas canvas, Bitmap bitmap, int align){ 26 | int canvasWidth = canvas.getWidth(); 27 | int canvasHeight = canvas.getHeight(); 28 | int bitmapWidth = bitmap.getWidth(); 29 | int bitmapHeight = bitmap.getHeight(); 30 | float left = 0; 31 | float top = 0; 32 | 33 | if(align == APNG_ANIM_ALIGN_TOP) { 34 | //默认距离顶部距离为0 35 | left = canvasWidth - bitmapWidth > 0 ? (float) (canvasWidth - bitmapWidth) / 2 : (float) (bitmapWidth - canvasWidth) / 2; 36 | top = 0; 37 | 38 | } 39 | else if(align == APNG_ANIM_ALIGN_MIDDLE){ 40 | left = canvasWidth - bitmapWidth > 0 ? (float) (canvasWidth - bitmapWidth) / 2 : (float) (bitmapWidth - canvasWidth) / 2; 41 | top = canvasHeight - bitmapHeight > 0 ? (float) (canvasHeight - bitmapHeight) / 2 : (float) (bitmapHeight - canvasHeight) / 2; 42 | } 43 | else if(align == APNG_ANIM_ALIGN_BOTTOM){ 44 | left = canvasWidth - bitmapWidth > 0 ? (float) (canvasWidth - bitmapWidth) / 2 : (float) (bitmapWidth - canvasWidth) / 2; 45 | top = canvasHeight - bitmapHeight > 0 ? (float) (canvasHeight - bitmapHeight): (float) (bitmapHeight - canvasHeight); 46 | } 47 | 48 | return new float[]{left, top}; 49 | 50 | } 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | /** 59 | * 获取中心点的坐标 60 | * @param canvas 61 | * @param bitmap 62 | * @param align 63 | * @return 64 | */ 65 | public static float[] getCenterCoordinate(Canvas canvas, Bitmap bitmap, int align, float mScaling){ 66 | float canvasWidth = canvas.getWidth(); 67 | float canvasHeight = canvas.getHeight(); 68 | float bitmapWidth = bitmap.getWidth() * mScaling; 69 | float bitmapHeight = bitmap.getHeight() * mScaling; 70 | float postX = canvasWidth / 2; 71 | float postY = 0; 72 | 73 | 74 | if(align == APNG_ANIM_ALIGN_TOP) { 75 | postY = bitmapHeight / 2; 76 | 77 | } 78 | else if(align == APNG_ANIM_ALIGN_MIDDLE){ 79 | postY = canvasHeight / 2; 80 | } 81 | else if(align == APNG_ANIM_ALIGN_BOTTOM){ 82 | postY = canvasHeight - bitmapHeight / 2; 83 | } 84 | 85 | return new float[]{postX, postY}; 86 | 87 | } 88 | 89 | public static float[] getTranLeftAndTop(Canvas canvas, Bitmap bitmap, int align, float mScaling, float percent){ 90 | float canvasWidth = canvas.getWidth(); 91 | float canvasHeight = canvas.getHeight(); 92 | float bitmapWidth = bitmap.getWidth() * mScaling; 93 | float bitmapHeight = bitmap.getHeight() * mScaling; 94 | 95 | //因为按宽的比例进行缩放,有可能图片的高度比Canvas的高度大 96 | bitmapWidth = bitmapWidth > canvasWidth ? canvasWidth:bitmapWidth; 97 | bitmapHeight = bitmapHeight > canvasHeight ? canvasHeight:bitmapHeight; 98 | 99 | float tranLeft = 0; 100 | float tranTop = 0; 101 | //不是按比例进行缩放 102 | if(percent == -1) { 103 | if (align == APNG_ANIM_ALIGN_TOP) { 104 | //默认距离顶部距离为0 105 | tranLeft = canvasWidth - bitmapWidth > 0 ? (canvasWidth - bitmapWidth) / 2 : (bitmapWidth - canvasWidth) / 2; 106 | tranTop = 0; 107 | 108 | } else if (align == APNG_ANIM_ALIGN_MIDDLE) { 109 | tranLeft = canvasWidth - bitmapWidth > 0 ? (canvasWidth - bitmapWidth) / 2 : (bitmapWidth - canvasWidth) / 2; 110 | tranTop = canvasHeight - bitmapHeight > 0 ? (canvasHeight - bitmapHeight) / 2 : (bitmapHeight - canvasHeight) / 2; 111 | } else if (align == APNG_ANIM_ALIGN_BOTTOM) { 112 | tranLeft = canvasWidth - bitmapWidth > 0 ? (canvasWidth - bitmapWidth) / 2 : (bitmapWidth - canvasWidth) / 2; 113 | tranTop = canvasHeight - bitmapHeight > 0 ? (canvasHeight - bitmapHeight) : (bitmapHeight - canvasHeight); 114 | } 115 | 116 | } 117 | else{ 118 | if (align == APNG_ANIM_ALIGN_TOP) { 119 | tranLeft = canvasWidth - bitmapWidth > 0 ? (canvasWidth - bitmapWidth) / 2 : (bitmapWidth - canvasWidth) / 2; 120 | tranTop = canvasHeight * percent; 121 | 122 | } else if (align == APNG_ANIM_ALIGN_MIDDLE) { 123 | tranLeft = canvasWidth - bitmapWidth > 0 ? (canvasWidth - bitmapWidth) / 2 : (bitmapWidth - canvasWidth) / 2; 124 | tranTop = canvasHeight * percent - bitmapHeight/ 2; 125 | } else if (align == APNG_ANIM_ALIGN_BOTTOM) { 126 | tranLeft = canvasWidth - bitmapWidth > 0 ? (canvasWidth - bitmapWidth) / 2 : (bitmapWidth - canvasWidth) / 2; 127 | tranTop = canvasHeight * percent - bitmapHeight; 128 | } 129 | } 130 | 131 | 132 | return new float[]{tranLeft, tranTop}; 133 | 134 | } 135 | 136 | 137 | 138 | 139 | } 140 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.apng.utils; 2 | 3 | import android.content.*; 4 | import android.graphics.*; 5 | import android.os.*; 6 | import android.text.*; 7 | import ar.com.hjg.pngj.*; 8 | import com.apng.utils.RecyclingUtils.*; 9 | 10 | import java.io.*; 11 | import java.net.*; 12 | 13 | /** 14 | * @author xing.hu 15 | * @since 2016/3/26, 12:15 16 | */ 17 | public class FileUtils { 18 | public static String getBaseName(String filename) { 19 | return removeExtension(getName(filename)); 20 | } 21 | 22 | public static String getName(String filename) { 23 | if (filename == null) { 24 | return null; 25 | } else { 26 | int index = indexOfLastSeparator(filename); 27 | return filename.substring(index + 1); 28 | } 29 | } 30 | 31 | public static int indexOfLastSeparator(String filename) { 32 | if (filename == null) { 33 | return -1; 34 | } else { 35 | int lastUnixPos = filename.lastIndexOf(47); 36 | int lastWindowsPos = filename.lastIndexOf(92); 37 | return Math.max(lastUnixPos, lastWindowsPos); 38 | } 39 | } 40 | 41 | public static String removeExtension(String filename) { 42 | if (filename == null) { 43 | return null; 44 | } else { 45 | int index = indexOfExtension(filename); 46 | return index == -1 ? filename : filename.substring(0, index); 47 | } 48 | } 49 | 50 | public static int indexOfExtension(String filename) { 51 | if (filename == null) { 52 | return -1; 53 | } else { 54 | int extensionPos = filename.lastIndexOf(46); 55 | int lastSeparator = indexOfLastSeparator(filename); 56 | return lastSeparator > extensionPos ? -1 : extensionPos; 57 | } 58 | } 59 | 60 | public static String getExtension(String filename) { 61 | if (filename == null) { 62 | return null; 63 | } else { 64 | int index = indexOfExtension(filename); 65 | return index == -1 ? "" : filename.substring(index + 1); 66 | } 67 | } 68 | 69 | public static boolean isApng(File file) { 70 | boolean isApng = false; 71 | 72 | try { 73 | PngReaderApng reader = new PngReaderApng(file); 74 | reader.end(); 75 | 76 | //int apngNumFrames = reader.getApngNumFrames(); 77 | 78 | //isApng = apngNumFrames > 1; 79 | isApng = reader.isApng(); 80 | 81 | } catch (Exception e) { 82 | e.printStackTrace(); 83 | } 84 | 85 | return isApng; 86 | } 87 | 88 | 89 | private static Bitmap decodeFile(String path, int maxWidth, int maxHeight) { 90 | if (TextUtils.isEmpty(path)) { 91 | return null; 92 | } 93 | 94 | BitmapFactory.Options options = new BitmapFactory.Options(); 95 | options.inJustDecodeBounds = false; 96 | Bitmap bitmap = null; 97 | try { 98 | bitmap = BitmapFactory.decodeFile(path, options); 99 | } catch (Throwable e) { 100 | e.printStackTrace(); 101 | } finally { 102 | } 103 | return bitmap; 104 | } 105 | 106 | public static int[] getApngWH(String path) { 107 | if (TextUtils.isEmpty(path)) { 108 | return null; 109 | } 110 | 111 | BitmapFactory.Options options = new BitmapFactory.Options(); 112 | options.inJustDecodeBounds = true; 113 | 114 | try { 115 | Bitmap bitmap = BitmapFactory.decodeFile(path, options); 116 | } catch (Throwable e) { 117 | e.printStackTrace(); 118 | } 119 | return new int[]{options.outWidth, options.outHeight}; 120 | } 121 | 122 | /** 123 | * 把文件从Asset复制到缓存 124 | * @param imageUri 125 | * @return 126 | */ 127 | public static File processApngFile(String imageUri, Context context) { 128 | if(TextUtils.isEmpty(imageUri)) return null; 129 | String path = ApngDownloadUtil.getFileCachePath(imageUri, context); 130 | File cacheFile = null; 131 | if(!TextUtils.isEmpty(path)) { 132 | cacheFile = new File(path); 133 | if (!cacheFile.exists()) { 134 | Scheme scheme = Scheme.ofUri(imageUri); 135 | InputStream source; 136 | if (scheme == Scheme.ASSETS) { 137 | try { 138 | source = getStreamFromAssets(imageUri, context); 139 | copyInputStreamToFile(source, cacheFile); 140 | } catch (IOException e) { 141 | e.printStackTrace(); 142 | } 143 | } else { 144 | source = null; 145 | try { 146 | URL source1 = new URL(imageUri); 147 | InputStream e = source1.openStream(); 148 | copyInputStreamToFile(e, cacheFile); 149 | } catch (MalformedURLException e) { 150 | e.printStackTrace(); 151 | } catch (IOException e) { 152 | e.printStackTrace(); 153 | } catch (NetworkOnMainThreadException e) { 154 | e.printStackTrace(); 155 | } 156 | } 157 | } 158 | } 159 | 160 | return cacheFile; 161 | } 162 | 163 | public static InputStream getStreamFromAssets(String imageUri, Context context) throws IOException { 164 | String filePath = RecyclingUtils.Scheme.ASSETS.crop(imageUri); 165 | return context.getAssets().open(filePath); 166 | } 167 | 168 | public static boolean copyInputStreamToFile(InputStream inputStream, File destination) { 169 | try { 170 | FileOutputStream e = new FileOutputStream(destination, false); 171 | byte[] bt = new byte[1024]; 172 | 173 | int c; 174 | while((c = inputStream.read(bt)) > 0) { 175 | e.write(bt, 0, c); 176 | } 177 | 178 | e.close(); 179 | inputStream.close(); 180 | return true; 181 | } catch (FileNotFoundException e) { 182 | e.printStackTrace(); 183 | } catch (IOException e) { 184 | e.printStackTrace(); 185 | } 186 | 187 | return false; 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/utils/Md5.java: -------------------------------------------------------------------------------- 1 | package com.apng.utils; 2 | 3 | 4 | import java.io.*; 5 | import java.security.*; 6 | 7 | public final class Md5 { 8 | 9 | public static String getFileMD5String(File file) throws IOException { 10 | MessageDigest md5; 11 | try { 12 | md5 = MessageDigest.getInstance("MD5"); 13 | InputStream fis; 14 | fis = new FileInputStream(file); 15 | byte[] buffer = new byte[1024]; 16 | int numRead = 0; 17 | while ((numRead = fis.read(buffer)) > 0) { 18 | md5.update(buffer, 0, numRead); 19 | } 20 | fis.close(); 21 | return bufferToHex(md5.digest()); 22 | } catch (NoSuchAlgorithmException e) { 23 | e.printStackTrace(); 24 | } 25 | return null; 26 | } 27 | 28 | public static String toMD5(String s) { 29 | if (s != null) { 30 | try { 31 | byte[] bs = s.getBytes("UTF-8"); 32 | return encrypt(bs); 33 | } catch (UnsupportedEncodingException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | return null; 38 | } 39 | 40 | public static String md5Hex(String s) { 41 | if (s != null) { 42 | try { 43 | byte[] bs = s.getBytes("UTF-8"); 44 | MessageDigest md5 = MessageDigest.getInstance("MD5"); 45 | md5.update(bs); 46 | byte[] md5Bytes = md5.digest(); 47 | return bufferToHex(md5Bytes); 48 | } catch (UnsupportedEncodingException e) { 49 | e.printStackTrace(); 50 | } catch (NoSuchAlgorithmException e) { 51 | e.printStackTrace(); 52 | } 53 | } 54 | return null; 55 | } 56 | 57 | private synchronized static String encrypt(byte[] obj) { 58 | try { 59 | MessageDigest md5 = MessageDigest.getInstance("MD5"); 60 | md5.update(obj); 61 | byte[] bs = md5.digest(); 62 | StringBuilder sb = new StringBuilder(); 63 | for (int i = 0; i < bs.length; i++) { 64 | sb.append(Integer.toHexString((0x000000ff & bs[i]) | 0xffffff00).substring(6)); 65 | } 66 | return sb.toString(); 67 | } catch (NoSuchAlgorithmException e) { 68 | e.printStackTrace(); 69 | } 70 | return null; 71 | } 72 | 73 | private static String bufferToHex(byte bytes[]) { 74 | return bufferToHex(bytes, 0, bytes.length); 75 | } 76 | 77 | private static String bufferToHex(byte bytes[], int m, int n) { 78 | StringBuffer stringbuffer = new StringBuffer(2 * n); 79 | int k = m + n; 80 | for (int l = m; l < k; l++) { 81 | appendHexPair(bytes[l], stringbuffer); 82 | } 83 | return stringbuffer.toString(); 84 | } 85 | 86 | private static void appendHexPair(byte bt, StringBuffer stringbuffer) { 87 | char c0 = hexDigits[(bt & 0xf0) >> 4]; // 取字节中高 4 位的数字转换, 88 | // >>>为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同 89 | char c1 = hexDigits[bt & 0xf]; // 取字节中低 4 位的数字转换 90 | stringbuffer.append(c0); 91 | stringbuffer.append(c1); 92 | } 93 | 94 | protected static char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 95 | 'f' }; 96 | 97 | } 98 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/utils/RecyclingUtils.java: -------------------------------------------------------------------------------- 1 | package com.apng.utils; 2 | 3 | 4 | 5 | public class RecyclingUtils { 6 | 7 | public static final String SEPARATOR = "*"; 8 | 9 | /** 10 | * Represents supported schemes(protocols) of URI. Provides convenient 11 | * methods for work with schemes and URIs. 12 | */ 13 | public static enum Scheme { 14 | HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS( 15 | "assets"), DRAWABLE("drawable"), UNKNOWN(""); 16 | 17 | private String scheme; 18 | private String uriPrefix; 19 | 20 | Scheme(String scheme) { 21 | this.scheme = scheme; 22 | uriPrefix = scheme + "://"; 23 | } 24 | 25 | /** 26 | * Defines scheme of incoming URI 27 | * 28 | * @param uri 29 | * URI for scheme detection 30 | * @return Scheme of incoming URI 31 | */ 32 | public static Scheme ofUri(String uri) { 33 | if (uri != null) { 34 | for (Scheme s : values()) { 35 | if (s.belongsTo(uri)) { 36 | return s; 37 | } 38 | } 39 | } 40 | return UNKNOWN; 41 | } 42 | 43 | private boolean belongsTo(String uri) { 44 | return uri.startsWith(uriPrefix); 45 | } 46 | 47 | /** Appends scheme to incoming path */ 48 | public String wrap(String path) { 49 | return uriPrefix + path; 50 | } 51 | 52 | /** Removed scheme part ("scheme://") from incoming URI */ 53 | public String crop(String uri) { 54 | if (!belongsTo(uri)) { 55 | throw new IllegalArgumentException(String.format( 56 | "URI [%1$s] doesn't have expected scheme [%2$s]", uri, 57 | scheme)); 58 | } 59 | return uri.substring(uriPrefix.length()); 60 | } 61 | } 62 | 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/view/ApngImageView.java: -------------------------------------------------------------------------------- 1 | package com.apng.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Matrix; 9 | import android.graphics.Paint; 10 | import android.graphics.PaintFlagsDrawFilter; 11 | import android.graphics.PorterDuff; 12 | import android.graphics.drawable.Animatable; 13 | import android.os.Handler; 14 | import android.os.Message; 15 | import android.os.Process; 16 | import android.util.AttributeSet; 17 | import android.util.Log; 18 | import android.widget.ImageView; 19 | 20 | import com.apng.ApngACTLChunk; 21 | import com.apng.ApngFrame; 22 | import com.apng.ApngFrameRender; 23 | import com.apng.ApngReader; 24 | import com.apng.entity.AnimParams; 25 | import com.apng.utils.ApngUtils; 26 | 27 | import java.io.ByteArrayOutputStream; 28 | import java.io.FilterInputStream; 29 | import java.io.IOException; 30 | import java.io.InputStream; 31 | import java.lang.ref.WeakReference; 32 | 33 | 34 | /** 35 | * @author xing.hu 36 | * @since 2016/11/3, 下午3:07 37 | * ImageView support Apng play 38 | */ 39 | public class ApngImageView extends ImageView implements Animatable { 40 | public static final String TAG = ApngImageView.class.getSimpleName(); 41 | private static final float DELAY_FACTOR = 1000F; 42 | public static int HALF_TRANSPARENT = Color.parseColor("#7F000000"); 43 | 44 | // start a thread to play the Apng Animation 45 | 46 | private AnimParams mAnimParams; 47 | 48 | //apng assist class, support apng Render 49 | private ApngPlayAssist mApngPlayAssist; 50 | 51 | private ApngHandler mApngHandler; 52 | 53 | private volatile AnimationListener mListener; 54 | 55 | private volatile int mStep = ApngLoader.Const.STEP_DEFAULT; 56 | 57 | 58 | private static class ApngHandler extends Handler { 59 | private WeakReference mRef; 60 | 61 | public ApngHandler(ApngImageView apngImageView) { 62 | mRef = new WeakReference<>(apngImageView); 63 | 64 | } 65 | 66 | @Override 67 | public void handleMessage(Message msg) { 68 | super.handleMessage(msg); 69 | } 70 | } 71 | 72 | 73 | @Override 74 | public void start() { 75 | mApngPlayAssist.play(); 76 | 77 | } 78 | 79 | @Override 80 | public void stop() { 81 | mApngPlayAssist.stop(); 82 | 83 | } 84 | 85 | @Override 86 | public boolean isRunning() { 87 | return false; 88 | } 89 | 90 | public interface AnimationListener { 91 | /** 92 | * call back when the anim plays complete 93 | */ 94 | void onAnimationCompleted(); 95 | } 96 | 97 | public void setAnimationListener(AnimationListener mListener) { 98 | this.mListener = mListener; 99 | } 100 | 101 | public ApngImageView(Context context) { 102 | super(context); 103 | init(context); 104 | 105 | } 106 | 107 | public ApngImageView(Context context, AttributeSet attrs) { 108 | super(context, attrs); 109 | init(context); 110 | 111 | 112 | } 113 | 114 | public ApngImageView(Context context, AttributeSet attrs, int defStyleAttr) { 115 | super(context, attrs); 116 | init(context); 117 | 118 | } 119 | 120 | 121 | 122 | 123 | @Override 124 | protected void onDraw(Canvas canvas) { 125 | super.onDraw(canvas); 126 | switch (mStep){ 127 | case ApngLoader.Const.STEP_CLEAR_CANVAS: 128 | mApngPlayAssist.clearCanvas(canvas); 129 | break; 130 | case ApngLoader.Const.STEP_DRAW_FRAME: 131 | mApngPlayAssist.drawFrame(canvas); 132 | break; 133 | default: 134 | break; 135 | 136 | } 137 | 138 | } 139 | 140 | private void init(Context context) { 141 | mApngPlayAssist = new ApngPlayAssist(); 142 | mApngHandler = new ApngHandler(this); 143 | setLayerType(LAYER_TYPE_HARDWARE, null); 144 | mStep = ApngLoader.Const.STEP_DEFAULT; 145 | 146 | 147 | } 148 | 149 | /** 150 | * set tha Apng Item to the queue 151 | */ 152 | public void setApngForPlay(AnimParams animItem) { 153 | mAnimParams = animItem; 154 | mStep = ApngLoader.Const.STEP_DEFAULT; 155 | } 156 | 157 | 158 | @Override 159 | protected void onAttachedToWindow() { 160 | Log.d(TAG, "onAttachedToWindow()"); 161 | super.onAttachedToWindow(); 162 | } 163 | 164 | @Override 165 | protected void onDetachedFromWindow() { 166 | super.onDetachedFromWindow(); 167 | mApngPlayAssist.detach(); 168 | 169 | } 170 | 171 | 172 | 173 | 174 | private class ApngPlayAssist implements Runnable { 175 | 176 | private float mScale; 177 | 178 | private ApngFrameRender mFrameRender; 179 | 180 | private ApngFrame curFrame; 181 | 182 | private Bitmap curFrameBmp; 183 | 184 | private volatile boolean mIsPlay = false; 185 | 186 | private static final int MAX_ZERO_NUM = 3; 187 | 188 | 189 | private void play() { 190 | mIsPlay = true; 191 | ApngLoader.getInstance().getExecutor().execute(this); 192 | } 193 | 194 | 195 | private void stop() { 196 | mIsPlay = false; 197 | mStep = ApngLoader.Const.STEP_CLEAR_CANVAS; 198 | notifyPlayCompeleted(); 199 | } 200 | 201 | 202 | private void detach(){ 203 | if(mIsPlay) { 204 | mIsPlay = false; 205 | ApngLoader.getInstance().getExecutor().remove(this); 206 | } 207 | } 208 | 209 | 210 | @Override 211 | public void run() { 212 | if (mAnimParams == null) { 213 | return; 214 | } 215 | Log.d(TAG, "PlayThread run()"); 216 | try { 217 | // play it 218 | playAnimation(); 219 | 220 | // play end 221 | stop(); 222 | 223 | } catch (InterruptedException e) { 224 | Log.e(TAG, Log.getStackTraceString(e)); 225 | 226 | } finally { 227 | mFrameRender.recycle(); 228 | } 229 | } 230 | 231 | /** 232 | * play the apng animation 233 | */ 234 | private void playAnimation() throws InterruptedException { 235 | try { 236 | mFrameRender = new ApngFrameRender(); 237 | // step 1: prepare 238 | ApngReader reader = new ApngReader(mAnimParams.imagePath); 239 | ApngACTLChunk actl = reader.getACTL(); 240 | if (mAnimParams.isHasBackground) setBgColor(true); 241 | // all loop count = apng_internal_loop_count x apng_play_times 242 | // if apng_internal_loop_count == 0 then set it to 1 (not support loop indefinitely) 243 | int loopCount = mAnimParams.loopCount * (actl.getNumPlays() == 0 ? 1 : actl.getNumPlays()); 244 | 245 | // step 2: draw frames 246 | boolean isLoop = loopCount == AnimParams.PLAY_4_LOOP; 247 | 248 | for (int lc = 0; lc < loopCount || isLoop; lc++) { 249 | // reallocated to head again if loops more the one time 250 | if (lc > 0 || isLoop) reader.reset(); 251 | for (int i = 0; i < actl.getNumFrames(); i++) { 252 | long start = System.currentTimeMillis(); 253 | // get frame data 254 | curFrame = reader.nextFrame(); 255 | if (curFrame == null) break; // if read next frame failed, break loop 256 | 257 | byte[] data = readStream(curFrame.getImageStream()); 258 | 259 | if (data != null) { 260 | //Bitmap frameBmp = BitmapFactory.decodeStream(frame.getImageStream()); 261 | 262 | curFrameBmp = BitmapFactory.decodeByteArray(data, 0, data.length); 263 | 264 | Log.d(TAG, "read the " + i + " frame:" + (System.currentTimeMillis() - start) + "ms"); 265 | 266 | // init the render and calculate scale rate 267 | // at first time get the frame width and height 268 | if (lc == 0 && i == 0) { 269 | int imgW = curFrame.getWidth(), imgH = curFrame.getHeight(); 270 | mScale = calculateScale(mAnimParams.scaleType, imgW, imgH, getWidth(), getHeight()); 271 | mFrameRender.prepare(imgW, imgH); 272 | } 273 | 274 | 275 | index++; 276 | mStep = ApngLoader.Const.STEP_DRAW_FRAME; 277 | ApngImageView.this.postInvalidate(); 278 | 279 | // delay 280 | int waitMillis = Math.round(curFrame.getDelayNum() * DELAY_FACTOR / curFrame.getDelayDen()) 281 | - (int) (System.currentTimeMillis() - start); 282 | Thread.sleep(waitMillis > 0 ? waitMillis : 0); 283 | } 284 | 285 | } 286 | } 287 | 288 | } catch (Exception e) { 289 | Log.e(TAG, Log.getStackTraceString(e)); 290 | } finally { 291 | if (mAnimParams.isHasBackground) setBgColor(false); 292 | } 293 | } 294 | 295 | private void setBgColor(final boolean show) { 296 | ApngImageView.this.post(new Runnable() { 297 | @Override 298 | public void run() { 299 | if (show) 300 | ApngImageView.this.setBackgroundColor(HALF_TRANSPARENT); 301 | else 302 | ApngImageView.this.setBackgroundColor(Color.TRANSPARENT); 303 | } 304 | }); 305 | } 306 | 307 | private void notifyPlayCompeleted() { 308 | if (mListener == null) return; 309 | ApngImageView.this.post(new Runnable() { 310 | @Override 311 | public void run() { 312 | if (mListener != null) 313 | mListener.onAnimationCompleted(); 314 | } 315 | }); 316 | } 317 | 318 | /* 319 | * get image byte stream 320 | * */ 321 | private byte[] readStream(InputStream inStream) throws Exception { 322 | ByteArrayOutputStream outStream = new ByteArrayOutputStream(); 323 | byte[] buffer = new byte[1024]; 324 | int len = 0; 325 | 326 | //fix bug: the end of inputStream while return 0 if the type of phone is Meizu MEIZU E3 327 | int numZero = 0; 328 | 329 | while ((len = inStream.read(buffer)) != -1) { 330 | outStream.write(buffer, 0, len); 331 | if (len == 0) { 332 | numZero++; 333 | if (numZero >= MAX_ZERO_NUM) { 334 | break; 335 | } 336 | } 337 | } 338 | outStream.close(); 339 | inStream.close(); 340 | return outStream.toByteArray(); 341 | } 342 | 343 | public class PatchInputStream extends FilterInputStream { 344 | 345 | protected PatchInputStream(InputStream in) { 346 | super(in); 347 | // TODO Auto-generated constructor stub 348 | } 349 | 350 | public long skip(long n) throws IOException { 351 | long m = 0l; 352 | while (m < n) { 353 | long _m = in.skip(n - m); 354 | if (_m == 0l) { 355 | break; 356 | } 357 | m += _m; 358 | } 359 | return m; 360 | } 361 | 362 | } 363 | 364 | /** 365 | * calculate the ratio of image width to canvas 366 | */ 367 | private float calculateScale(int scaleType, int imgW, int imgH, int viewW, int viewH) { 368 | if (scaleType == AnimParams.WIDTH_SCALE_TYPE) { 369 | return ((float) viewW) / imgW; 370 | } else if (scaleType == AnimParams.HEIGHT_SCALE_TYPE) { 371 | return ((float) viewH) / imgH; 372 | } else if (scaleType == AnimParams.WIDTH_OR_HEIGHT_SCALE_TYPE) { 373 | float scalingByWidth = ((float) viewW) / imgW; 374 | float scalingByHeight = ((float) viewH) / imgH; 375 | return scalingByWidth <= scalingByHeight ? scalingByWidth : scalingByHeight; 376 | } 377 | return 1F; 378 | } 379 | 380 | int index = 0; 381 | 382 | 383 | /** 384 | * draw the appointed frame 385 | */ 386 | private void drawFrame(Canvas canvas) { 387 | 388 | if (mIsPlay 389 | && mFrameRender != null 390 | && curFrame != null 391 | && curFrameBmp != null) { 392 | 393 | //start to draw the frame 394 | try { 395 | Matrix matrix = new Matrix(); 396 | matrix.setScale(mScale, mScale); 397 | Bitmap bmp = mFrameRender.render(curFrame, curFrameBmp); 398 | 399 | //saveBitmap(bmp, index); 400 | 401 | 402 | //anti-aliasing 403 | canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 404 | float[] tranLeftAndTop = ApngUtils.getTranLeftAndTop(canvas, bmp, mAnimParams.align, mScale, mAnimParams.percent); 405 | canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG)); 406 | matrix.postTranslate(tranLeftAndTop[0], tranLeftAndTop[1]); 407 | canvas.drawBitmap(bmp, matrix, null); 408 | curFrameBmp.recycle(); 409 | } catch (Exception e) { 410 | Log.e(TAG, "draw error msg:" + Log.getStackTraceString(e)); 411 | } 412 | } 413 | } 414 | 415 | 416 | /** 417 | * clear canvas 418 | * 419 | * @param canvas 420 | */ 421 | public void clearCanvas(Canvas canvas) { 422 | try { 423 | canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 424 | } catch (Exception e) { 425 | Log.e(TAG, "draw error msg:" + Log.getStackTraceString(e)); 426 | } 427 | } 428 | 429 | 430 | 431 | } 432 | 433 | 434 | } 435 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/view/ApngLoader.java: -------------------------------------------------------------------------------- 1 | package com.apng.view; 2 | 3 | import com.apng.entity.AnimParams; 4 | 5 | import java.util.concurrent.ScheduledThreadPoolExecutor; 6 | import java.util.concurrent.ThreadPoolExecutor; 7 | 8 | /** 9 | * @author xing.hu 10 | * @since 2019-12-06, 16:23 11 | * Apng Loader 12 | * 13 | */ 14 | public class ApngLoader { 15 | 16 | 17 | 18 | private ScheduledThreadPoolExecutor mExecutors; 19 | 20 | private ApngLoader(){ 21 | mExecutors = new ScheduledThreadPoolExecutor(1, new ThreadPoolExecutor.DiscardPolicy()); 22 | 23 | } 24 | 25 | private static class Holder { 26 | private static ApngLoader apngLoader = new ApngLoader(); 27 | 28 | } 29 | 30 | public static ApngLoader getInstance() { 31 | return Holder.apngLoader; 32 | 33 | } 34 | 35 | public void loadApng(String apngPath, ApngImageView view){ 36 | AnimParams animItem1 = new AnimParams(); 37 | animItem1.imagePath = apngPath; 38 | animItem1.loopCount = AnimParams.PLAY_4_LOOP; 39 | view.setApngForPlay(animItem1); 40 | view.start(); 41 | 42 | } 43 | 44 | 45 | 46 | public ScheduledThreadPoolExecutor getExecutor(){ 47 | return mExecutors; 48 | } 49 | 50 | 51 | public static class Const{ 52 | public static final int STEP_DEFAULT = 0; 53 | // clear ImageView canvas 54 | public static final int STEP_CLEAR_CANVAS = 1; 55 | // draw frame on ImageView canvas 56 | public static final int STEP_DRAW_FRAME = 2; 57 | 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /apng/src/main/java/com/apng/view/ApngSurfaceView.java: -------------------------------------------------------------------------------- 1 | package com.apng.view; 2 | 3 | import android.content.*; 4 | import android.graphics.*; 5 | import android.os.Process; 6 | import android.util.*; 7 | import android.view.*; 8 | 9 | import com.apng.ApngACTLChunk; 10 | import com.apng.ApngFrame; 11 | import com.apng.ApngFrameRender; 12 | import com.apng.ApngReader; 13 | import com.apng.entity.AnimParams; 14 | import com.apng.utils.ApngUtils; 15 | 16 | 17 | import java.io.*; 18 | import java.util.concurrent.*; 19 | 20 | 21 | /** 22 | * @author xing.hu 23 | * @since 2016/11/3, 下午3:07 24 | */ 25 | public class ApngSurfaceView extends SurfaceView implements SurfaceHolder.Callback { 26 | public static final String TAG = ApngSurfaceView.class.getSimpleName(); 27 | private static final float DELAY_FACTOR = 1000F; 28 | public static boolean enableVerboseLog = false; 29 | public static int HALF_TRANSPARENT = Color.parseColor("#7F000000"); 30 | 31 | // start a thread to play the Apng Animation 32 | private PlayThread mPlayThread; 33 | private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); 34 | 35 | private volatile AnimationListener mListener; 36 | 37 | public interface AnimationListener { 38 | /** 39 | * call back when the anim plays complete 40 | */ 41 | void onAnimationCompleted(); 42 | } 43 | 44 | public void setAnimationListener(AnimationListener mListener) { 45 | this.mListener = mListener; 46 | } 47 | 48 | public ApngSurfaceView(Context context) { 49 | super(context); 50 | init(context); 51 | 52 | } 53 | 54 | public ApngSurfaceView(Context context, AttributeSet attrs) { 55 | super(context, attrs); 56 | init(context); 57 | 58 | 59 | } 60 | 61 | public ApngSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { 62 | super(context, attrs); 63 | init(context); 64 | 65 | } 66 | 67 | 68 | 69 | 70 | private void init(Context context) { 71 | setZOrderOnTop(true); 72 | setLayerType(LAYER_TYPE_HARDWARE, null); 73 | getHolder().addCallback(this); 74 | getHolder().setFormat(PixelFormat.TRANSLUCENT); 75 | 76 | enableVerboseLog = true; 77 | 78 | 79 | 80 | } 81 | 82 | /** 83 | * add tha Apng Item to the queue 84 | */ 85 | public void addApngForPlay(AnimParams giftAnimItem) { 86 | queue.add(giftAnimItem); 87 | } 88 | 89 | @Override 90 | public void surfaceDestroyed(SurfaceHolder arg0) { 91 | mPlayThread.setSurfaceEnabled(false); 92 | } 93 | 94 | @Override 95 | public void surfaceCreated(SurfaceHolder arg0) { 96 | mPlayThread.setSurfaceEnabled(true); 97 | 98 | } 99 | 100 | @Override 101 | public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { 102 | } 103 | 104 | @Override 105 | protected void onAttachedToWindow() { 106 | Log.d(TAG, "onAttachedToWindow()"); 107 | super.onAttachedToWindow(); 108 | mPlayThread = new PlayThread(); 109 | mPlayThread.start(); 110 | } 111 | 112 | @Override 113 | protected void onDetachedFromWindow() { 114 | super.onDetachedFromWindow(); 115 | mPlayThread.interrupt(); 116 | mPlayThread = null; 117 | } 118 | 119 | private class PlayThread extends Thread { 120 | private volatile boolean surfaceEnabled; 121 | private ApngFrameRender mFrameRender; 122 | private float mScale; 123 | private static final int MAX_ZERO_NUM = 3; 124 | 125 | public PlayThread() { 126 | Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); 127 | } 128 | 129 | @Override 130 | public void run() { 131 | Log.d(TAG, "PlayThread run()"); 132 | mFrameRender = new ApngFrameRender(); 133 | try { 134 | while (!isInterrupted()) { 135 | try { 136 | // step1: fetch an animation object 137 | AnimParams animItem = queue.take(); 138 | 139 | if(animItem.name.equals("fire")){ 140 | //mediaPlayer.start(); 141 | } 142 | 143 | // step2: play it 144 | playAnimation(animItem); 145 | 146 | 147 | // clear canvas when played last apng 148 | if (queue.isEmpty()) { 149 | clearCanvas(); 150 | notifyPlayCompeleted(); 151 | //mediaPlayer.stop(); 152 | } 153 | } catch (InterruptedException e) { 154 | Log.e(TAG, Log.getStackTraceString(e)); 155 | break; // waiting in queue has been interrupted, finish play thread 156 | } 157 | } 158 | } finally { 159 | mFrameRender.recycle(); 160 | } 161 | } 162 | 163 | /** 164 | * play the apng animation 165 | */ 166 | private void playAnimation(AnimParams animItem) throws InterruptedException { 167 | try { 168 | // step 1: prepare 169 | ApngReader reader = new ApngReader(animItem.imagePath); 170 | ApngACTLChunk actl = reader.getACTL(); 171 | if (animItem.isHasBackground) setBgColor(true); 172 | // all loop count = apng_internal_loop_count x apng_play_times 173 | // if apng_internal_loop_count == 0 then set it to 1 (not support loop indefinitely) 174 | int loopCount = animItem.loopCount * (actl.getNumPlays() == 0 ? 1 : actl.getNumPlays()); 175 | 176 | // step 2: draw frames 177 | boolean isLoop = loopCount == AnimParams.PLAY_4_LOOP ; 178 | 179 | for (int lc = 0; lc < loopCount || isLoop; lc++) { 180 | // reallocated to head again if loops more the one time 181 | if (lc > 0 || isLoop) reader.reset(); 182 | for (int i = 0; i < actl.getNumFrames(); i++) { 183 | long start = System.currentTimeMillis(); 184 | // get frame data 185 | ApngFrame frame = reader.nextFrame(); 186 | if (frame == null) break; // if read next frame failed, break loop 187 | 188 | byte[] data = readStream(frame.getImageStream()); 189 | 190 | if(data!=null){ 191 | //Bitmap frameBmp = BitmapFactory.decodeStream(frame.getImageStream()); 192 | 193 | Bitmap frameBmp = BitmapFactory.decodeByteArray(data, 0, data.length); 194 | 195 | Log.d(TAG, "read the " + i + " frame:" + (System.currentTimeMillis() - start) + "ms"); 196 | 197 | // init the render and calculate scale rate 198 | // at first time get the frame width and height 199 | if (lc == 0 && i == 0) { 200 | int imgW = frame.getWidth(), imgH = frame.getHeight(); 201 | mScale = calculateScale(animItem.scaleType, imgW, imgH, getWidth(), getHeight()); 202 | mFrameRender.prepare(imgW, imgH); 203 | } 204 | 205 | // draw frame 206 | drawFrame(animItem, frame, frameBmp); 207 | frameBmp.recycle(); 208 | 209 | // delay 210 | int waitMillis = Math.round(frame.getDelayNum() * DELAY_FACTOR / frame.getDelayDen()) 211 | - (int) (System.currentTimeMillis() - start); 212 | sleep(waitMillis > 0 ? waitMillis : 0); 213 | } 214 | 215 | } 216 | } 217 | 218 | } catch (Exception e) { 219 | Log.e(TAG, Log.getStackTraceString(e)); 220 | } 221 | finally { 222 | if (animItem.isHasBackground) setBgColor(false); 223 | } 224 | } 225 | 226 | private void setBgColor(final boolean show) { 227 | ApngSurfaceView.this.post(new Runnable() { 228 | @Override 229 | public void run() { 230 | if (show) 231 | ApngSurfaceView.this.setBackgroundColor(HALF_TRANSPARENT); 232 | else 233 | ApngSurfaceView.this.setBackgroundColor(Color.TRANSPARENT); 234 | } 235 | }); 236 | } 237 | 238 | private void notifyPlayCompeleted() { 239 | if (mListener == null) return; 240 | ApngSurfaceView.this.post(new Runnable() { 241 | @Override 242 | public void run() { 243 | if (mListener != null) 244 | mListener.onAnimationCompleted(); 245 | } 246 | }); 247 | } 248 | 249 | /* 250 | * get image byte stream 251 | * */ 252 | private byte[] readStream(InputStream inStream) throws Exception { 253 | ByteArrayOutputStream outStream = new ByteArrayOutputStream(); 254 | byte[] buffer = new byte[1024]; 255 | int len = 0; 256 | 257 | //fix bug: the end of inputStream while return 0 if the type of phone is Meizu MEIZU E3 258 | int numZero = 0; 259 | 260 | while ((len = inStream.read(buffer)) != -1) { 261 | outStream.write(buffer, 0, len); 262 | if(len == 0 ) { 263 | numZero++; 264 | if(numZero >= MAX_ZERO_NUM){ 265 | break; 266 | } 267 | } 268 | } 269 | outStream.close(); 270 | inStream.close(); 271 | return outStream.toByteArray(); 272 | } 273 | 274 | public class PatchInputStream extends FilterInputStream{ 275 | 276 | protected PatchInputStream(InputStream in) { 277 | super(in); 278 | // TODO Auto-generated constructor stub 279 | } 280 | 281 | public long skip(long n)throws IOException{ 282 | long m=0l; 283 | while(m 2 | apng 3 | 4 | -------------------------------------------------------------------------------- /apng/src/test/java/test/sakhu/com/apng/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package test.sakhu.com.apng; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | defaultConfig { 6 | applicationId "com.saku.test" 7 | minSdkVersion 14 8 | targetSdkVersion 23 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | sourceSets { main { assets.srcDirs = ['src/main/assets', 'src/main/assets/'] } } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile project(":apng") 24 | } 25 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/saku/test/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.saku.test; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.saku.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/assets/car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/assets/car.png -------------------------------------------------------------------------------- /app/src/main/assets/color_ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/assets/color_ball.png -------------------------------------------------------------------------------- /app/src/main/java/com/saku/test/ApngImageViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.saku.test; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | 8 | import com.apng.entity.AnimParams; 9 | import com.apng.utils.FileUtils; 10 | import com.apng.view.ApngImageView; 11 | import com.apng.view.ApngLoader; 12 | 13 | import java.io.File; 14 | 15 | public class ApngImageViewActivity extends Activity{ 16 | private ApngImageView mApngImageView; 17 | private static final String COLOR_BALL_IMAGE_PATH = "assets://color_ball.png"; 18 | private static final String CAR_IMAGE_PATH = "assets://car.png"; 19 | 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_image_view); 25 | mApngImageView = (ApngImageView) findViewById(R.id.apng_image_view); 26 | Button startPlay = (Button) findViewById(R.id.start_play); 27 | startPlay.setOnClickListener(new View.OnClickListener() { 28 | @Override 29 | public void onClick(View v) { 30 | playAnim(); 31 | } 32 | }); 33 | } 34 | 35 | 36 | private void playAnim(){ 37 | //File file = FileUtils.processApngFile(COLOR_BALL_IMAGE_PATH, this); 38 | File file1 = FileUtils.processApngFile(COLOR_BALL_IMAGE_PATH, this); 39 | 40 | ApngLoader.getInstance().loadApng(file1.getAbsolutePath(), mApngImageView); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/saku/test/ApngSurfaceViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.saku.test; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | 8 | import com.apng.entity.AnimParams; 9 | import com.apng.utils.FileUtils; 10 | import com.apng.view.ApngSurfaceView; 11 | 12 | import java.io.File; 13 | 14 | public class ApngSurfaceViewActivity extends Activity{ 15 | private ApngSurfaceView mApngSurfaceView; 16 | private static final String COLOR_BALL_IMAGE_PATH = "assets://color_ball.png"; 17 | private static final String CAR_IMAGE_PATH = "assets://car.png"; 18 | 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_surface_view); 24 | mApngSurfaceView = (ApngSurfaceView)findViewById(R.id.apng_surface_view); 25 | Button startPlay = (Button) findViewById(R.id.start_play); 26 | startPlay.setOnClickListener(new View.OnClickListener() { 27 | @Override 28 | public void onClick(View v) { 29 | playAnim(); 30 | } 31 | }); 32 | } 33 | 34 | 35 | private void playAnim(){ 36 | //File file = FileUtils.processApngFile(COLOR_BALL_IMAGE_PATH, this); 37 | File file1 = FileUtils.processApngFile(CAR_IMAGE_PATH, this); 38 | 39 | /* if(file == null) return; 40 | AnimParams animItem = new AnimParams(); 41 | animItem.align = 2; 42 | animItem.imagePath = file.getAbsolutePath(); 43 | animItem.isHasBackground = true; 44 | animItem.percent = 0.5f; 45 | mApngSurfaceView.addApngForPlay(animItem);*/ 46 | 47 | 48 | AnimParams animItem1 = new AnimParams(); 49 | animItem1.align = 2; 50 | animItem1.imagePath = file1.getAbsolutePath(); 51 | animItem1.isHasBackground = true; 52 | animItem1.percent = 0.5f; 53 | animItem1.loopCount = AnimParams.PLAY_4_LOOP; 54 | mApngSurfaceView.addApngForPlay(animItem1); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/saku/test/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.saku.test; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.Button; 8 | 9 | import com.apng.view.ApngSurfaceView; 10 | import com.apng.entity.AnimParams; 11 | import com.apng.utils.FileUtils; 12 | 13 | import java.io.File; 14 | 15 | public class MainActivity extends Activity{ 16 | 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | 23 | 24 | findViewById(R.id.image_view_btn).setOnClickListener(new View.OnClickListener() { 25 | @Override 26 | public void onClick(View v) { 27 | Intent intent = new Intent(MainActivity.this, ApngImageViewActivity.class); 28 | startActivity(intent); 29 | } 30 | }); 31 | 32 | findViewById(R.id.surface_view_btn).setOnClickListener(new View.OnClickListener() { 33 | @Override 34 | public void onClick(View v) { 35 | Intent intent = new Intent(MainActivity.this, ApngSurfaceViewActivity.class); 36 | startActivity(intent); 37 | } 38 | }); 39 | } 40 | 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_image_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 |