├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 | 
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 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
20 |
21 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_surface_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SakuApng
3 | play
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/test/java/com/saku/test/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.saku.test;
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 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.1.1'
11 |
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/demo.mp4
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeNanStar/SakuApng/3ba5532750c83b27021fee0ff86082fa977febec/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Aug 24 16:14:38 CST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':apng'
2 |
--------------------------------------------------------------------------------