├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── drawable-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── drawable-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── drawable-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── styles.xml
│ │ │ ├── dimens.xml
│ │ │ └── strings.xml
│ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ ├── menu
│ │ │ └── main.xml
│ │ └── layout
│ │ │ ├── activity_grid.xml
│ │ │ └── activity_main.xml
│ │ ├── java
│ │ └── com
│ │ │ └── felipecsl
│ │ │ └── gifimageview
│ │ │ └── app
│ │ │ ├── GifDataDownloader.kt
│ │ │ ├── GridViewActivity.kt
│ │ │ ├── ByteArrayHttpClient.kt
│ │ │ ├── GifGridAdapter.kt
│ │ │ ├── Blur.kt
│ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── library
├── .gitignore
├── gradle.properties
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── felipecsl
│ │ └── gifimageview
│ │ └── library
│ │ ├── SimpleBitmapProvider.kt
│ │ ├── GifFrame.kt
│ │ ├── GifHeader.kt
│ │ ├── GifImageView.kt
│ │ ├── GifHeaderParser.kt
│ │ └── GifDecoder.kt
├── proguard-rules.txt
└── build.gradle
├── settings.gradle
├── demo.gif
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .travis.yml
├── LICENSE
├── .buildscript
└── deploy_snapshot.sh
├── CHANGELOG.md
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':library', ':app'
2 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipecsl/GifImageView/HEAD/demo.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | local.properties
3 | .DS_Store
4 | .idea
5 | *.iml
6 | build/
7 |
--------------------------------------------------------------------------------
/library/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=GifImageView library
2 | POM_ARTIFACT_ID=gifimageview
3 | POM_PACKAGING=aar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipecsl/GifImageView/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipecsl/GifImageView/HEAD/app/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipecsl/GifImageView/HEAD/app/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipecsl/GifImageView/HEAD/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipecsl/GifImageView/HEAD/app/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Mar 31 23:48:41 EEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 | 150dp
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | GifImageView Demo
5 | Grid Example
6 |
7 | Stop Gif
8 | Start Gif
9 | Clear Gif
10 | Blur Gif
11 |
12 | https://i0.wp.com/www.printmag.com/wp-content/uploads/2021/02/4cbe8d_f1ed2800a49649848102c68fc5a66e53mv2.gif?fit=476%2C280&ssl=1
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_grid.xml:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/library/proguard-rules.txt:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the ProGuard
5 | # include property in project.properties.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
--------------------------------------------------------------------------------
/app/src/main/java/com/felipecsl/gifimageview/app/GifDataDownloader.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.app
2 |
3 | import android.util.Log
4 |
5 | object GifDataDownloader {
6 | private const val TAG = "GifDataDownloader"
7 | @JvmStatic
8 | fun downloadGifData(gifUrl: String?, callback: GifDataDownloaderCallback?) {
9 | if (gifUrl == null) return
10 | Thread {
11 | try {
12 | val gifData = ByteArrayHttpClient.get(gifUrl)
13 | callback?.onGifDownloaded(gifData)
14 | } catch (e: OutOfMemoryError) {
15 | Log.e(TAG, "GifDecode OOM: $gifUrl", e)
16 | }
17 | }.start()
18 | }
19 |
20 | interface GifDataDownloaderCallback {
21 | fun onGifDownloaded(gifData: ByteArray?)
22 | }
23 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 | android:
3 | components:
4 | - tools
5 | - platform-tools
6 | - build-tools-27.0.1
7 | - android-27
8 | - extra-android-m2repository
9 | - extra-android-support
10 | - extra-google-m2repository
11 | after_success:
12 | - ".buildscript/deploy_snapshot.sh"
13 | jdk:
14 | - oraclejdk8
15 | branches:
16 | except:
17 | - gh-pages
18 | notifications:
19 | email: false
20 | sudo: false
21 | cache:
22 | directories:
23 | - "$HOME/.m2"
24 | env:
25 | global:
26 | - secure: TPzHKYKcPm/u4+rOWDmHXJ80cyYckqtJkdh+94NePdSXjjaKn7KeS3XbSguQnlmQqpt/iQ5KN0lVQK3rGUHugw8ZynoPUNgf3wH9ZhcAqrd3m41dWvNwEexBtNJt/XX3D2xsa1083iRuJntHhMOodh/uuwsc30hVibjMsCyQKQk=
27 | - secure: idTB9agcZHiB8DA0vgASUSxYTyXPU1pdMTwVet3DZ372rbxsRJMQg1DJn77NrldxAfX53sB5xtCwpRFyg0xoPYZJXfq/uTSA2Ym7kjFc2miVjhVHubn4CCWkazLjvkeLeuXrXa6poRA6VD1WTLDYw8HKcT2J19sWWkhtR0tDqGE=
28 |
--------------------------------------------------------------------------------
/library/src/main/java/com/felipecsl/gifimageview/library/SimpleBitmapProvider.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.library
2 |
3 | import android.graphics.Bitmap
4 | import com.felipecsl.gifimageview.library.GifDecoder.BitmapProvider
5 |
6 | internal class SimpleBitmapProvider : BitmapProvider {
7 | override fun obtain(width: Int, height: Int, config: Bitmap.Config?): Bitmap {
8 | return Bitmap.createBitmap(width, height, config)
9 | }
10 |
11 | override fun release(bitmap: Bitmap?) {
12 | bitmap!!.recycle()
13 | }
14 |
15 | override fun obtainByteArray(size: Int): ByteArray? {
16 | return ByteArray(size)
17 | }
18 |
19 | override fun release(bytes: ByteArray?) {
20 | // no-op
21 | }
22 |
23 | override fun obtainIntArray(size: Int): IntArray? {
24 | return IntArray(size)
25 | }
26 |
27 | override fun release(array: IntArray?) {
28 | // no-op
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/felipecsl/gifimageview/app/GridViewActivity.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.app
2 |
3 | import android.os.Bundle
4 | import android.widget.GridView
5 | import androidx.appcompat.app.AppCompatActivity
6 | import java.util.ArrayList
7 |
8 | class GridViewActivity : AppCompatActivity() {
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContentView(R.layout.activity_grid)
12 | val imageUrls: MutableList = ArrayList(NUMBER_CELLS)
13 | for (i in 0 until NUMBER_CELLS) {
14 | imageUrls.add("https://cloud.githubusercontent.com/assets/4410820/11539468/c4d62a9c-9959-11e5-908e-cf50a21ac0e9.gif")
15 | }
16 | val adapter = GifGridAdapter(this, imageUrls)
17 | val gridView = findViewById(R.id.gridView)
18 | gridView.adapter = adapter
19 | }
20 |
21 | companion object {
22 | private const val NUMBER_CELLS = 50
23 | }
24 | }
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | //apply from: 'gradle-mvn-push.gradle'
4 |
5 | android {
6 | compileSdkVersion 27
7 |
8 | defaultConfig {
9 | minSdkVersion 14
10 | }
11 |
12 | compileOptions {
13 | sourceCompatibility JavaVersion.VERSION_1_7
14 | targetCompatibility JavaVersion.VERSION_1_7
15 | }
16 | }
17 |
18 | dependencies {
19 | // implementation 'com.android.support:support-annotations:27.1.1'
20 | // implementation "androidx.core:core-ktx:+"
21 | // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
22 |
23 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
24 | implementation 'androidx.core:core-ktx:1.9.0'
25 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
26 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
27 |
28 | implementation 'androidx.appcompat:appcompat:1.6.1'
29 | implementation 'com.google.android.material:material:1.8.0'
30 | }
31 | repositories {
32 | mavenCentral()
33 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Felipe Lima
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/.buildscript/deploy_snapshot.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
4 | #
5 | # Adapted from https://coderwall.com/p/9b_lfq and
6 | # http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
7 |
8 | SLUG="felipecsl/GifImageView"
9 | JDK="oraclejdk8"
10 | BRANCH="master"
11 |
12 | set -e
13 |
14 | if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then
15 | echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'."
16 | elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then
17 | echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'."
18 | elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
19 | echo "Skipping snapshot deployment: was pull request."
20 | elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then
21 | echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'."
22 | else
23 | echo "Deploying snapshot..."
24 | ./gradlew uploadArchives -PNEXUS_USERNAME="${CI_DEPLOY_USERNAME}" -PNEXUS_PASSWORD="${CI_DEPLOY_PASSWORD}" -PCI="${CI}"
25 | echo "Snapshot deployed!"
26 | fi
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlinx-serialization'
4 |
5 | android {
6 | compileSdkVersion 33
7 |
8 | defaultConfig {
9 | minSdkVersion 14
10 | targetSdkVersion 33
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | renderscriptTargetApi 19
15 | renderscriptSupportModeEnabled true
16 | }
17 |
18 | lintOptions {
19 | disable 'InvalidPackage'
20 | }
21 |
22 | compileOptions {
23 | sourceCompatibility JavaVersion.VERSION_1_8
24 | targetCompatibility JavaVersion.VERSION_1_8
25 | }
26 |
27 | kotlinOptions {
28 | jvmTarget = '1.8'
29 | }
30 |
31 | buildTypes {
32 | release {
33 | minifyEnabled false
34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
35 | }
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | api project(':library')
42 |
43 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
44 | implementation 'androidx.core:core-ktx:1.9.0'
45 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
46 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
47 |
48 | implementation 'androidx.appcompat:appcompat:1.6.1'
49 | implementation 'com.google.android.material:material:1.8.0'
50 |
51 | implementation 'com.squareup.okhttp3:okhttp:3.9.1'
52 | implementation 'commons-io:commons-io:2.6'
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/felipecsl/gifimageview/app/ByteArrayHttpClient.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.app
2 |
3 | import android.util.Log
4 | import okhttp3.OkHttpClient
5 | import okhttp3.Request
6 | import org.apache.commons.io.IOUtils
7 | import java.io.IOException
8 | import java.io.InputStream
9 | import java.io.UnsupportedEncodingException
10 | import java.net.MalformedURLException
11 | import java.net.URL
12 | import java.net.URLDecoder
13 |
14 | object ByteArrayHttpClient {
15 | private const val TAG = "ByteArrayHttpClient"
16 | private val CLIENT = OkHttpClient()
17 | operator fun get(urlString: String?): ByteArray? {
18 | var inpustStream: InputStream? = null
19 | try {
20 | val decodedUrl = URLDecoder.decode(urlString, "UTF-8")
21 | val url = URL(decodedUrl)
22 | val request = Request.Builder().url(url).build()
23 | val response = CLIENT.newCall(request).execute()
24 | inpustStream = response.body()?.byteStream()
25 | return IOUtils.toByteArray(inpustStream)
26 | } catch (e: MalformedURLException) {
27 | Log.d(TAG, "Malformed URL", e)
28 | } catch (e: OutOfMemoryError) {
29 | Log.d(TAG, "Out of memory", e)
30 | } catch (e: UnsupportedEncodingException) {
31 | Log.d(TAG, "Unsupported encoding", e)
32 | } catch (e: IOException) {
33 | Log.d(TAG, "IO exception", e)
34 | } finally {
35 | if (inpustStream != null) {
36 | try {
37 | inpustStream.close()
38 | } catch (ignored: IOException) {
39 | }
40 | }
41 | }
42 | return null
43 | }
44 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 2.2.0 (11/27/2017)
2 |
3 | * New (#66): Expose `GifDecoder#getFrameCount`
4 | * New (#65): Bump compile and target SDK versions to 27, `minSdkVersion` is now 14
5 | * Fix (#58): Apply latest changes from Glide's `GifDecoder`, fix `DISPOSE_BACKGROUND` and `DISPOSE_PREVIOUS`
6 | * New (#57): Added `OnAnimationStart` callback
7 | * New (#35): Added support for GIFs with a loop count specified
8 |
9 | # 2.1.0 (06/22/2016)
10 |
11 | * Fix (#34): Clear animation when detached from the window
12 | * New (#18): On stop callback for when animation completes
13 | * Fix (#36): Division by Zero Exception
14 | * Fix (#37): IllegalArgumentException due to overflowing buffer length
15 | * Fix (#28): Memory leak in cleanupRunnable
16 |
17 | # 2.0.0 (10/27/2015)
18 |
19 | * Ported the `GifDecoder` implementation from [Glide](https://github.com/bumptech/glide) which fixes
20 | most of the gif weirdnesses and bugs.
21 |
22 | # 1.2.0
23 |
24 | * Adds support for custom frame display duration via `setFramesDisplayDuration()`, thanks to @donvigo
25 |
26 | # 1.1.0
27 |
28 | * Adds support to ``OnFrameAvailable`` callback, thanks to @luciofm.
29 |
30 | # 1.0.6
31 |
32 | * Adds methods ``getGifWidth()`` and ``getGifHeight()`` to ``GifImageView`` class.
33 |
34 | # 1.0.5
35 |
36 | * Catch exceptions in ``GifImageView.run()``
37 |
38 | # 1.0.4
39 |
40 | * Safer clear() routine to avoid null pointer exceptions.
41 |
42 | # 1.0.3
43 |
44 | * PR #5: Fixes crashes when clearing the GIF bytes.
45 |
46 | # 1.0.2
47 |
48 | * PR #4: Replace false image data decoding in readContents with simple skipping.
49 |
50 | # 1.0.1
51 |
52 | * Adds ``isAnimating()`` that returns whether the gif animation is currently running.
53 |
54 | # 1.0.0
55 |
56 | Initial release
--------------------------------------------------------------------------------
/app/src/main/java/com/felipecsl/gifimageview/app/GifGridAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.app
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.AbsListView
7 | import android.widget.BaseAdapter
8 | import android.widget.ImageView
9 | import com.felipecsl.gifimageview.app.GifDataDownloader.GifDataDownloaderCallback
10 | import com.felipecsl.gifimageview.app.GifDataDownloader.downloadGifData
11 | import com.felipecsl.gifimageview.library.GifImageView
12 |
13 | class GifGridAdapter(private val context: Context, private val imageUrls: List) :
14 | BaseAdapter() {
15 | override fun getCount(): Int {
16 | return imageUrls.size
17 | }
18 |
19 | override fun getItem(position: Int): Any {
20 | return imageUrls[position]
21 | }
22 |
23 | override fun getItemId(position: Int): Long {
24 | return 0
25 | }
26 |
27 | override fun getView(position: Int, convertView: View, parent: ViewGroup): View {
28 | val imageView: GifImageView
29 | if (convertView == null) {
30 | imageView = GifImageView(context)
31 | imageView.scaleType = ImageView.ScaleType.CENTER
32 | imageView.setPadding(10, 10, 10, 10)
33 | val size = AbsListView.LayoutParams.WRAP_CONTENT
34 | val layoutParams = AbsListView.LayoutParams(size, size)
35 | imageView.layoutParams = layoutParams
36 | } else {
37 | imageView = convertView as GifImageView
38 | imageView.clear()
39 | }
40 | downloadGifData(imageUrls[position], object : GifDataDownloaderCallback {
41 | override fun onGifDownloaded(bytes: ByteArray?) {
42 | // Do something with the downloaded GIF data
43 | imageView.setBytes(bytes)
44 | imageView.startAnimation()
45 | }
46 | })
47 | return imageView
48 | }
49 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Settings specified in this file will override any Gradle settings
5 | # configured through the IDE.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 | VERSION_CODE=6
20 | VERSION_NAME=2.3.0-SNAPSHOT
21 | GROUP=com.felipecsl
22 |
23 | POM_DESCRIPTION=Android ImageView that handles animated GIF images
24 | POM_URL=https://github.com/felipecsl/GifImageView/tree/new-scv
25 | POM_SCM_URL=https://github.com/felipecsl/GifImageView/tree/new-scv
26 | POM_SCM_CONNECTION=scm:git@github.com:felipecsl/GifImageView.git
27 | POM_SCM_DEV_CONNECTION=scm:git@github.com:felipecsl/GifImageView.git
28 | POM_LICENCE_NAME=The MIT License (MIT)
29 | POM_LICENCE_URL=https://github.com/felipecsl/GifImageView/blob/master/LICENSE
30 | POM_LICENCE_DIST=repo
31 | POM_DEVELOPER_ID=felipecsl
32 | POM_DEVELOPER_NAME=Felipe Lima
33 | org.gradle.configureondemand=false
34 |
35 | # AndroidX package structure to make it clearer which packages are bundled with the
36 | # Android operating system, and which are packaged with your app"s APK
37 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
38 | android.useAndroidX=true
39 | # Automatically convert third-party libraries to use AndroidX
40 | android.enableJetifier=true
41 | # Kotlin code style for this project: "official" or "obsolete":
42 | kotlin.code.style=official
--------------------------------------------------------------------------------
/library/src/main/java/com/felipecsl/gifimageview/library/GifFrame.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014 Google, Inc. All rights reserved.
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.felipecsl.gifimageview.library
20 |
21 | /**
22 | * Inner model class housing metadata for each frame.
23 | */
24 | class GifFrame {
25 | @JvmField
26 | var ix = 0
27 | @JvmField
28 | var iy = 0
29 | @JvmField
30 | var iw = 0
31 | @JvmField
32 | var ih = 0
33 |
34 | /**
35 | * Control Flag.
36 | */
37 | @JvmField
38 | var interlace = false
39 |
40 | /**
41 | * Control Flag.
42 | */
43 | @JvmField
44 | var transparency = false
45 |
46 | /**
47 | * Disposal Method.
48 | */
49 | @JvmField
50 | var dispose = 0
51 |
52 | /**
53 | * Transparency Index.
54 | */
55 | @JvmField
56 | var transIndex = 0
57 |
58 | /**
59 | * Delay, in ms, to next frame.
60 | */
61 | @JvmField
62 | var delay = 0
63 |
64 | /**
65 | * Index in the raw buffer where we need to start reading to decode.
66 | */
67 | @JvmField
68 | var bufferFrameStart = 0
69 |
70 | /**
71 | * Local Color Table.
72 | */
73 | @JvmField
74 | var lct: IntArray? = null
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/felipecsl/gifimageview/app/Blur.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.app
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import androidx.renderscript.Allocation
6 | import androidx.renderscript.Element
7 | import androidx.renderscript.RenderScript
8 | import androidx.renderscript.ScriptIntrinsicBlur
9 |
10 | class Blur private constructor(context: Context) {
11 | private val rs: RenderScript
12 | private var script: ScriptIntrinsicBlur? = null
13 | private var input: Allocation? = null
14 | private var output: Allocation? = null
15 | private var configured = false
16 | private var tmp: Bitmap? = null
17 | private lateinit var pixels: IntArray
18 | fun blur(image: Bitmap?): Bitmap? {
19 | var bitmapImage: Bitmap? = image ?: return null
20 | bitmapImage = bitmapImage?.let { RGB565toARGB888(it) }
21 | if (!configured) {
22 | input = Allocation.createFromBitmap(rs, bitmapImage)
23 | output = Allocation.createTyped(rs, input?.type)
24 | script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
25 | script?.setRadius(BLUR_RADIUS)
26 | configured = true
27 | } else input?.copyFrom(bitmapImage)
28 | script?.setInput(input)
29 | script?.forEach(output)
30 | output?.copyTo(bitmapImage)
31 | return bitmapImage
32 | }
33 |
34 | private fun RGB565toARGB888(img: Bitmap): Bitmap? {
35 | val numPixels = img.width * img.height
36 |
37 | //Create a Bitmap of the appropriate format.
38 | if (tmp == null) {
39 | tmp = Bitmap.createBitmap(img.width, img.height, Bitmap.Config.ARGB_8888)
40 | pixels = IntArray(numPixels)
41 | }
42 |
43 | //Get JPEG pixels. Each int is the color values for one pixel.
44 | img.getPixels(pixels, 0, img.width, 0, 0, img.width, img.height)
45 |
46 | //Set RGB pixels.
47 | tmp?.setPixels(pixels, 0, tmp!!.width, 0, 0, tmp!!.width, tmp!!.height)
48 | return tmp
49 | }
50 |
51 | companion object {
52 | private const val BLUR_RADIUS = 25f
53 | @JvmStatic
54 | fun newInstance(context: Context): Blur {
55 | return Blur(context)
56 | }
57 | }
58 |
59 | init {
60 | rs = RenderScript.create(context)
61 | }
62 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GifImageView
2 |
3 | Android ImageView that handles Animated GIF images
4 |
5 | [](https://android-arsenal.com/details/1/1339)
6 | [](https://travis-ci.org/felipecsl/GifImageView)
7 |
8 | ### Usage
9 |
10 | In your ``build.gradle`` file:
11 |
12 | ```groovy
13 | dependencies {
14 | implementation 'com.felipecsl:gifimageview:2.2.0'
15 | }
16 | ```
17 |
18 | In your `Activity` class:
19 |
20 | ```java
21 | @Override protected void onCreate(final Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | gifView = findViewById(R.id.gifImageView);
24 | gifView.setBytes(bitmapData);
25 | }
26 |
27 | @Override protected void onStart() {
28 | super.onStart();
29 | gifView.startAnimation();
30 | }
31 |
32 | @Override protected void onStop() {
33 | super.onStop();
34 | gifView.stopAnimation();
35 | }
36 | ```
37 |
38 | If you need to post-process the GIF frames, you can do that via ``GifImageView.setOnFrameAvailable()``.
39 | You can see an example of that in the sample app included on the repository.
40 |
41 | ```java
42 | gifImageView.setOnFrameAvailable(new GifImageView.OnFrameAvailable() {
43 | @Override public Bitmap onFrameAvailable(Bitmap bitmap) {
44 | return blurFilter.blur(bitmap);
45 | }
46 | });
47 | ```
48 |
49 | You can also reset an animation to play again from the beginning `gifImageView.resetAnimation();` or show a specific frame of the animation `gifImageView.gotoFrame(3)`;
50 |
51 | ### Demo
52 |
53 | 
54 |
55 | Be sure to also check the [demo project](https://github.com/felipecsl/GifImageView/blob/master/app/src/main/java/com/felipecsl/gifimageview/app/MainActivity.java) for a sample of usage!
56 |
57 | Snapshots of the development version are available in [Sonatype's `snapshots` repository](https://oss.sonatype.org/content/repositories/snapshots/).
58 |
59 | ### Contributing
60 |
61 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
62 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
63 | * Fork the project
64 | * Start a feature/bugfix branch
65 | * Commit and push until you are happy with your contribution
66 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
67 |
68 | ### Copyright and license
69 |
70 | Code and documentation copyright 2011- Felipe Lima.
71 | Code released under the [MIT license](https://github.com/felipecsl/GifImageView/blob/master/LICENSE.txt).
72 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
25 |
26 |
36 |
37 |
47 |
48 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/library/src/main/java/com/felipecsl/gifimageview/library/GifHeader.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2014 Google, Inc. All rights reserved.
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.felipecsl.gifimageview.library
20 |
21 |
22 | import kotlin.collections.ArrayList
23 |
24 | /**
25 | * A header object containing the number of frames in an animated GIF image as well as basic
26 | * metadata like width and height that can be used to decode each individual frame of the GIF. Can
27 | * be shared by one or more [GifDecoder]s to play the same animated GIF in multiple views.
28 | */
29 | class GifHeader {
30 | @JvmField
31 | var gct: IntArray? = null
32 |
33 | /**
34 | * Global status code of GIF data parsing.
35 | */
36 | @JvmField
37 | var status = GifDecoder.STATUS_OK
38 | var numFrames = 0
39 | @JvmField
40 | var currentFrame: GifFrame? = null
41 | @JvmField
42 | var frames: ArrayList = ArrayList()
43 |
44 | // Logical screen size.
45 | // Full image width.
46 | @JvmField
47 | var width = 0
48 |
49 | // Full image height.
50 | @JvmField
51 | var height = 0
52 |
53 | // 1 : global color table flag.
54 | @JvmField
55 | var gctFlag = false
56 |
57 | // 2-4 : color resolution.
58 | // 5 : gct sort flag.
59 | // 6-8 : gct size.
60 | @JvmField
61 | var gctSize = 0
62 |
63 | // Background color index.
64 | @JvmField
65 | var bgIndex = 0
66 |
67 | // Pixel aspect ratio.
68 | @JvmField
69 | var pixelAspect = 0
70 |
71 | //TODO: this is set both during reading the header and while decoding frames...
72 | @JvmField
73 | var bgColor = 0
74 | @JvmField
75 | var loopCount = 0
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/felipecsl/gifimageview/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.app
2 |
3 | import android.content.Intent
4 | import android.graphics.Bitmap
5 | import android.graphics.Color
6 | import android.os.Bundle
7 | import android.util.Log
8 | import android.view.Menu
9 | import android.view.MenuItem
10 | import android.widget.Toast
11 | import androidx.appcompat.app.AppCompatActivity
12 | import androidx.appcompat.widget.AppCompatButton
13 | import com.felipecsl.gifimageview.app.Blur.Companion.newInstance
14 | import com.felipecsl.gifimageview.app.GifDataDownloader.GifDataDownloaderCallback
15 | import com.felipecsl.gifimageview.app.GifDataDownloader.downloadGifData
16 | import com.felipecsl.gifimageview.library.GifImageView
17 | import com.felipecsl.gifimageview.library.GifImageView.OnAnimationStop
18 | import com.felipecsl.gifimageview.library.GifImageView.OnFrameAvailable
19 |
20 | class MainActivity : AppCompatActivity() {
21 |
22 | private var gifImageView: GifImageView? = null
23 | private var btnToggle: AppCompatButton? = null
24 | private var btnBlur: AppCompatButton? = null
25 | private var btnClear: AppCompatButton? = null
26 | private var shouldBlur = false
27 | private var blur: Blur? = null
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContentView(R.layout.activity_main)
32 |
33 | gifImageView = findViewById(R.id.gifImageView)
34 | btnToggle = findViewById(R.id.btnToggle)
35 | btnBlur = findViewById(R.id.btnBlur)
36 | btnClear = findViewById(R.id.btnClear)
37 | blur = newInstance(this)
38 |
39 | gifImageView?.onFrameAvailable = object : OnFrameAvailable {
40 | override fun onFrameAvailable(bitmap: Bitmap?): Bitmap? {
41 | return if (shouldBlur) {
42 | blur!!.blur(bitmap)
43 | } else bitmap
44 | }
45 | }
46 |
47 | gifImageView?.onAnimationStop = object : OnAnimationStop {
48 | override fun onAnimationStop() {
49 | runOnUiThread {
50 | Toast.makeText(
51 | this@MainActivity,
52 | "Animation stopped",
53 | Toast.LENGTH_SHORT
54 | ).show()
55 | }
56 | }
57 | }
58 | initClicks()
59 | initViewGif()
60 | setStyle()
61 | }
62 |
63 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
64 | menuInflater.inflate(R.menu.main, menu)
65 | return true
66 | }
67 |
68 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
69 | if (item.itemId == R.id.show_grid) {
70 | //startActivity(Intent(this, GridViewActivity::class.java))
71 | return true
72 | }
73 | return super.onOptionsItemSelected(item)
74 | }
75 |
76 | private fun initViewGif() {
77 | downloadGifData(
78 | getString(R.string.gif_url),
79 | object : GifDataDownloaderCallback {
80 | override fun onGifDownloaded(bytes: ByteArray?) {
81 | // Do something with the downloaded GIF data
82 | gifImageView?.setBytes(bytes)
83 | gifImageView?.startAnimation()
84 | Log.d(TAG, "GIF width is " + gifImageView?.getGifWidth())
85 | Log.d(TAG, "GIF height is " + gifImageView?.getGifHeight())
86 | }
87 | })
88 | }
89 |
90 | private fun initClicks() {
91 | btnToggle?.setOnClickListener {
92 | if (gifImageView?.isAnimating == true) {
93 | gifImageView?.stopAnimation()
94 | btnToggle?.text = getString(R.string.start_gif)
95 | } else {
96 | gifImageView?.startAnimation()
97 | btnToggle?.text = getString(R.string.stop_gif)
98 | }
99 | }
100 |
101 | btnClear?.setOnClickListener { gifImageView?.clear() }
102 |
103 | btnBlur?.setOnClickListener { shouldBlur = !shouldBlur }
104 | }
105 |
106 | companion object {
107 | private const val TAG = "MainActivity"
108 | }
109 |
110 | private fun setStyle(){
111 | btnToggle?.setBackgroundColor(Color.parseColor("#ff9e22"))
112 | btnBlur?.setBackgroundColor(Color.parseColor("#cd00ea"))
113 | btnClear?.setBackgroundColor(Color.parseColor("#D50000"))
114 |
115 |
116 | btnToggle?.setTextColor(Color.parseColor("#F3E5F5"))
117 | btnBlur?.setTextColor(Color.parseColor("#F3E5F5"))
118 | btnClear?.setTextColor(Color.parseColor("#F3E5F5"))
119 | }
120 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/src/main/java/com/felipecsl/gifimageview/library/GifImageView.kt:
--------------------------------------------------------------------------------
1 | package com.felipecsl.gifimageview.library
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.os.Handler
6 | import android.os.Looper
7 | import android.util.AttributeSet
8 | import android.util.Log
9 | import androidx.appcompat.widget.AppCompatImageView
10 | import java.lang.Exception
11 | import java.lang.IllegalArgumentException
12 |
13 | class GifImageView : AppCompatImageView, Runnable {
14 | private var gifDecoder: GifDecoder? = null
15 | private var tmpBitmap: Bitmap? = null
16 | private val myHandler = Handler(Looper.getMainLooper())
17 | var isAnimating = false
18 | private set
19 | private var renderFrame = false
20 | private var shouldClear = false
21 | private var animationThread: Thread? = null
22 | var onFrameAvailable: OnFrameAvailable? = null
23 |
24 | /**
25 | * Sets custom display duration in milliseconds for the all frames. Should be called before [ ][.startAnimation]
26 | *
27 | * @param framesDisplayDuration Duration in milliseconds. Default value = -1, this property will
28 | * be ignored and default delay from gif file will be used.
29 | */
30 | var framesDisplayDuration = -1L
31 | var onAnimationStop: OnAnimationStop? = null
32 | private var animationStartCallback: OnAnimationStart? = null
33 | private val updateResults = Runnable {
34 | if (tmpBitmap != null && !tmpBitmap!!.isRecycled) {
35 | setImageBitmap(tmpBitmap)
36 | }
37 | }
38 | private val cleanupRunnable = Runnable {
39 | tmpBitmap = null
40 | gifDecoder = null
41 | animationThread = null
42 | shouldClear = false
43 | }
44 |
45 | constructor(context: Context?, attrs: AttributeSet?) : super(
46 | context!!, attrs
47 | ) {
48 | }
49 |
50 | constructor(context: Context?) : super(context!!) {}
51 |
52 | fun setBytes(bytes: ByteArray?) {
53 | gifDecoder = GifDecoder()
54 | try {
55 | gifDecoder!!.read(bytes)
56 | } catch (e: Exception) {
57 | gifDecoder = null
58 | Log.e(TAG, e.message, e)
59 | return
60 | }
61 | if (isAnimating) {
62 | startAnimationThread()
63 | } else {
64 | gotoFrame(0)
65 | }
66 | }
67 |
68 | fun startAnimation() {
69 | isAnimating = true
70 | startAnimationThread()
71 | }
72 |
73 | fun stopAnimation() {
74 | isAnimating = false
75 | if (animationThread != null) {
76 | animationThread!!.interrupt()
77 | animationThread = null
78 | }
79 | }
80 |
81 | fun gotoFrame(frame: Int) {
82 | if (gifDecoder!!.currentFrameIndex == frame) return
83 | if (gifDecoder!!.setFrameIndex(frame - 1) && !isAnimating) {
84 | renderFrame = true
85 | startAnimationThread()
86 | }
87 | }
88 |
89 | fun resetAnimation() {
90 | gifDecoder!!.resetLoopIndex()
91 | gotoFrame(0)
92 | }
93 |
94 | fun clear() {
95 | isAnimating = false
96 | renderFrame = false
97 | shouldClear = true
98 | stopAnimation()
99 | myHandler.post(cleanupRunnable)
100 | }
101 |
102 | private fun canStart(): Boolean {
103 | return (isAnimating || renderFrame) && gifDecoder != null && animationThread == null
104 | }
105 |
106 | /**
107 | * Gets the number of frames read from file.
108 | *
109 | * @return frame count.
110 | */
111 | val frameCount: Int
112 | get() = gifDecoder!!.frameCount
113 |
114 |
115 |
116 | fun getGifWidth(): Int? {
117 | return gifDecoder?.getWidth()
118 | }
119 |
120 | fun getGifHeight(): Int? {
121 | return gifDecoder?.getHeight()
122 | }
123 |
124 | override fun run() {
125 | if (animationStartCallback != null) {
126 | animationStartCallback!!.onAnimationStart()
127 | }
128 | do {
129 | if (!isAnimating && !renderFrame) {
130 | break
131 | }
132 | val advance = gifDecoder!!.advance()
133 |
134 | //milliseconds spent on frame decode
135 | var frameDecodeTime: Long = 0
136 | try {
137 | val before = System.nanoTime()
138 | tmpBitmap = gifDecoder!!.nextFrame
139 | if (onFrameAvailable != null) {
140 | tmpBitmap = onFrameAvailable!!.onFrameAvailable(tmpBitmap)
141 | }
142 | frameDecodeTime = (System.nanoTime() - before) / 1000000
143 | myHandler .post(updateResults)
144 | } catch (e: ArrayIndexOutOfBoundsException) {
145 | Log.w(TAG, e)
146 | } catch (e: IllegalArgumentException) {
147 | Log.w(TAG, e)
148 | }
149 | renderFrame = false
150 | if (!isAnimating || !advance) {
151 | isAnimating = false
152 | break
153 | }
154 | try {
155 | var delay = gifDecoder!!.nextDelay.toLong()
156 | // Sleep for frame duration minus time already spent on frame decode
157 | // Actually we need next frame decode duration here,
158 | // but I use previous frame time to make code more readable
159 | delay -= frameDecodeTime.toLong()
160 | if (delay > 0) {
161 | Thread.sleep((if (framesDisplayDuration > 0) framesDisplayDuration else delay))
162 | }
163 | } catch (e: InterruptedException) {
164 | // suppress exception
165 | }
166 | } while (isAnimating)
167 | if (shouldClear) {
168 | myHandler.post(cleanupRunnable)
169 | }
170 | animationThread = null
171 | if (onAnimationStop != null) {
172 | onAnimationStop!!.onAnimationStop()
173 | }
174 | }
175 |
176 | interface OnFrameAvailable {
177 | fun onFrameAvailable(bitmap: Bitmap?): Bitmap?
178 | }
179 |
180 | fun setOnAnimationStart(animationStart: OnAnimationStart?) {
181 | animationStartCallback = animationStart
182 | }
183 |
184 | interface OnAnimationStop {
185 | fun onAnimationStop()
186 | }
187 |
188 | interface OnAnimationStart {
189 | fun onAnimationStart()
190 | }
191 |
192 | override fun onDetachedFromWindow() {
193 | super.onDetachedFromWindow()
194 | clear()
195 | }
196 |
197 | private fun startAnimationThread() {
198 | if (canStart()) {
199 | animationThread = Thread(this)
200 | animationThread!!.start()
201 | }
202 | }
203 |
204 | companion object {
205 | private const val TAG = "GifDecoderView"
206 | }
207 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/felipecsl/gifimageview/library/GifHeaderParser.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2014 Google, Inc. All rights reserved.
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.felipecsl.gifimageview.library
20 |
21 | import android.util.Log
22 | import com.felipecsl.gifimageview.library.GifHeaderParser
23 | import com.felipecsl.gifimageview.library.GifHeader
24 | import com.felipecsl.gifimageview.library.GifDecoder
25 | import java.lang.Exception
26 | import java.lang.IllegalArgumentException
27 | import java.nio.BufferUnderflowException
28 | import java.nio.ByteBuffer
29 | import java.nio.ByteOrder
30 | import java.util.*
31 |
32 | /**
33 | * A class responsible for creating [GifHeader]s from data
34 | * representing animated gifs.
35 | */
36 | class GifHeaderParser {
37 | // Raw data read working array.
38 | private val block = ByteArray(MAX_BLOCK_SIZE)
39 | private var rawData: ByteBuffer? = null
40 | private var header: GifHeader? = null
41 | private var blockSize = 0
42 | fun setData(data: ByteBuffer): GifHeaderParser {
43 | reset()
44 | rawData = data.asReadOnlyBuffer()
45 | rawData?.position(0)
46 | rawData?.order(ByteOrder.LITTLE_ENDIAN)
47 | return this
48 | }
49 |
50 | fun setData(data: ByteArray?): GifHeaderParser {
51 | if (data != null) {
52 | setData(ByteBuffer.wrap(data))
53 | } else {
54 | rawData = null
55 | header!!.status = GifDecoder.STATUS_OPEN_ERROR
56 | }
57 | return this
58 | }
59 |
60 | fun clear() {
61 | rawData = null
62 | header = null
63 | }
64 |
65 | private fun reset() {
66 | rawData = null
67 | Arrays.fill(block, 0.toByte())
68 | header = GifHeader()
69 | blockSize = 0
70 | }
71 |
72 | fun parseHeader(): GifHeader? {
73 | checkNotNull(rawData) { "You must call setData() before parseHeader()" }
74 | if (err()) {
75 | return header
76 | }
77 | readHeader()
78 | if (!err()) {
79 | readContents()
80 | if (header!!.numFrames < 0) {
81 | header!!.status = GifDecoder.STATUS_FORMAT_ERROR
82 | }
83 | }
84 | return header
85 | }/* maxFrames */
86 |
87 | /**
88 | * Determines if the GIF is animated by trying to read in the first 2 frames
89 | * This method reparses the data even if the header has already been read.
90 | */
91 | val isAnimated: Boolean
92 | get() {
93 | readHeader()
94 | if (!err()) {
95 | readContents(2 /* maxFrames */)
96 | }
97 | return header!!.numFrames > 1
98 | }
99 | /**
100 | * Main file parser. Reads GIF content blocks. Stops after reading maxFrames
101 | */
102 | /**
103 | * Main file parser. Reads GIF content blocks.
104 | */
105 | private fun readContents(maxFrames: Int = Int.MAX_VALUE) {
106 | // Read GIF file content blocks.
107 | var done = false
108 | while (!(done || err() || header!!.numFrames > maxFrames)) {
109 | var code = read()
110 | when (code) {
111 | 0x2C -> {
112 | // The graphics control extension is optional, but will always come first if it exists.
113 | // If one did
114 | // exist, there will be a non-null current frame which we should use. However if one
115 | // did not exist,
116 | // the current frame will be null and we must create it here. See issue #134.
117 | if (header!!.currentFrame == null) {
118 | header!!.currentFrame = GifFrame()
119 | }
120 | readBitmap()
121 | }
122 | 0x21 -> {
123 | code = read()
124 | when (code) {
125 | 0xf9 -> {
126 | // Start a new frame.
127 | header!!.currentFrame = GifFrame()
128 | readGraphicControlExt()
129 | }
130 | 0xff -> {
131 | readBlock()
132 | var app = ""
133 | var i = 0
134 | while (i < 11) {
135 | app += block[i].toChar()
136 | i++
137 | }
138 | if (app == "NETSCAPE2.0") {
139 | readNetscapeExt()
140 | } else {
141 | // Don't care.
142 | skip()
143 | }
144 | }
145 | 0xfe -> skip()
146 | 0x01 -> skip()
147 | else -> skip()
148 | }
149 | }
150 | 0x3b -> done = true
151 | 0x00 -> header!!.status = GifDecoder.STATUS_FORMAT_ERROR
152 | else -> header!!.status = GifDecoder.STATUS_FORMAT_ERROR
153 | }
154 | }
155 | }
156 |
157 | /**
158 | * Reads Graphics Control Extension values.
159 | */
160 | private fun readGraphicControlExt() {
161 | // Block size.
162 | read()
163 | // Packed fields.
164 | val packed = read()
165 | // Disposal method.
166 | header!!.currentFrame!!.dispose = packed and 0x1c shr 2
167 | if (header!!.currentFrame!!.dispose == 0) {
168 | // Elect to keep old image if discretionary.
169 | header!!.currentFrame!!.dispose = 1
170 | }
171 | header!!.currentFrame!!.transparency = packed and 1 != 0
172 | // Delay in milliseconds.
173 | var delayInHundredthsOfASecond = readShort()
174 | // TODO: consider allowing -1 to indicate show forever.
175 | if (delayInHundredthsOfASecond < MIN_FRAME_DELAY) {
176 | delayInHundredthsOfASecond = DEFAULT_FRAME_DELAY
177 | }
178 | header!!.currentFrame!!.delay = delayInHundredthsOfASecond * 10
179 | // Transparent color index
180 | header!!.currentFrame!!.transIndex = read()
181 | // Block terminator
182 | read()
183 | }
184 |
185 | /**
186 | * Reads next frame image.
187 | */
188 | private fun readBitmap() {
189 | // (sub)image position & size.
190 | header!!.currentFrame!!.ix = readShort()
191 | header!!.currentFrame!!.iy = readShort()
192 | header!!.currentFrame!!.iw = readShort()
193 | header!!.currentFrame!!.ih = readShort()
194 | val packed = read()
195 | // 1 - local color table flag interlace
196 | val lctFlag = packed and 0x80 != 0
197 | val lctSize = Math.pow(2.0, ((packed and 0x07) + 1).toDouble()).toInt()
198 | // 3 - sort flag
199 | // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color
200 | // table size
201 | header!!.currentFrame!!.interlace = packed and 0x40 != 0
202 | if (lctFlag) {
203 | // Read table.
204 | header!!.currentFrame!!.lct = readColorTable(lctSize)
205 | } else {
206 | // No local color table.
207 | header!!.currentFrame!!.lct = null
208 | }
209 |
210 | // Save this as the decoding position pointer.
211 | header!!.currentFrame!!.bufferFrameStart = rawData!!.position()
212 |
213 | // False decode pixel data to advance buffer.
214 | skipImageData()
215 | if (err()) {
216 | return
217 | }
218 | header!!.numFrames++
219 | // Add image to frame.
220 | header?.currentFrame?.let { header!!.frames.add(it) }
221 | }
222 |
223 | /**
224 | * Reads Netscape extension to obtain iteration count.
225 | */
226 | private fun readNetscapeExt() {
227 | do {
228 | readBlock()
229 | if (block[0].toInt() == 1) {
230 | // Loop count sub-block.
231 | val b1 = block[1].toInt() and 0xff
232 | val b2 = block[2].toInt() and 0xff
233 | header?.loopCount = b2 shl 8 or b1
234 | if (header?.loopCount == 0) {
235 | header?.loopCount = GifDecoder.LOOP_FOREVER
236 | }
237 | }
238 | } while (blockSize > 0 && !err())
239 | }
240 |
241 | /**
242 | * Reads GIF file header information.
243 | */
244 | private fun readHeader() {
245 | var id = ""
246 | for (i in 0..5) {
247 | id += read().toChar()
248 | }
249 | if (!id.startsWith("GIF")) {
250 | header!!.status = GifDecoder.STATUS_FORMAT_ERROR
251 | return
252 | }
253 | readLSD()
254 | if (header!!.gctFlag && !err()) {
255 | header!!.gct = readColorTable(header!!.gctSize)
256 | header!!.bgColor = header!!.gct!![header!!.bgIndex]
257 | }
258 | }
259 |
260 | /**
261 | * Reads Logical Screen Descriptor.
262 | */
263 | private fun readLSD() {
264 | // Logical screen size.
265 | header!!.width = readShort()
266 | header!!.height = readShort()
267 | // Packed fields
268 | val packed = read()
269 | // 1 : global color table flag.
270 | header!!.gctFlag = packed and 0x80 != 0
271 | // 2-4 : color resolution.
272 | // 5 : gct sort flag.
273 | // 6-8 : gct size.
274 | header!!.gctSize = 2 shl (packed and 7)
275 | // Background color index.
276 | header!!.bgIndex = read()
277 | // Pixel aspect ratio
278 | header!!.pixelAspect = read()
279 | }
280 |
281 | /**
282 | * Reads color table as 256 RGB integer values.
283 | *
284 | * @param ncolors int number of colors to read.
285 | * @return int array containing 256 colors (packed ARGB with full alpha).
286 | */
287 | private fun readColorTable(ncolors: Int): IntArray? {
288 | val nbytes = 3 * ncolors
289 | var tab: IntArray? = null
290 | val c = ByteArray(nbytes)
291 | try {
292 | rawData!![c]
293 |
294 | // TODO: what bounds checks are we avoiding if we know the number of colors?
295 | // Max size to avoid bounds checks.
296 | tab = IntArray(MAX_BLOCK_SIZE)
297 | var i = 0
298 | var j = 0
299 | while (i < ncolors) {
300 | val r = c[j++].toInt() and 0xff
301 | val g = c[j++].toInt() and 0xff
302 | val b = c[j++].toInt() and 0xff
303 | tab[i++] = -0x1000000 or (r shl 16) or (g shl 8) or b
304 | }
305 | } catch (e: BufferUnderflowException) {
306 | if (Log.isLoggable(TAG, Log.DEBUG)) {
307 | Log.d(TAG, "Format Error Reading Color Table", e)
308 | }
309 | header!!.status = GifDecoder.STATUS_FORMAT_ERROR
310 | }
311 | return tab
312 | }
313 |
314 | /**
315 | * Skips LZW image data for a single frame to advance buffer.
316 | */
317 | private fun skipImageData() {
318 | // lzwMinCodeSize
319 | read()
320 | // data sub-blocks
321 | skip()
322 | }
323 |
324 | /**
325 | * Skips variable length blocks up to and including next zero length block.
326 | */
327 | private fun skip() {
328 | try {
329 | var blockSize: Int
330 | do {
331 | blockSize = read()
332 | rawData!!.position(rawData!!.position() + blockSize)
333 | } while (blockSize > 0)
334 | } catch (ex: IllegalArgumentException) {
335 | }
336 | }
337 |
338 | /**
339 | * Reads next variable length block from input.
340 | *
341 | * @return number of bytes stored in "buffer"
342 | */
343 | private fun readBlock(): Int {
344 | blockSize = read()
345 | var n = 0
346 | if (blockSize > 0) {
347 | var count = 0
348 | try {
349 | while (n < blockSize) {
350 | count = blockSize - n
351 | rawData!![block, n, count]
352 | n += count
353 | }
354 | } catch (e: Exception) {
355 | if (Log.isLoggable(TAG, Log.DEBUG)) {
356 | Log.d(
357 | TAG,
358 | "Error Reading Block n: $n count: $count blockSize: $blockSize", e
359 | )
360 | }
361 | header!!.status = GifDecoder.STATUS_FORMAT_ERROR
362 | }
363 | }
364 | return n
365 | }
366 |
367 | /**
368 | * Reads a single byte from the input stream.
369 | */
370 | private fun read(): Int {
371 | var curByte = 0
372 | try {
373 | curByte = (rawData!!.get()).toInt() and 0xFF
374 | } catch (e: Exception) {
375 | header!!.status = GifDecoder.STATUS_FORMAT_ERROR
376 | }
377 | return curByte
378 | }
379 |
380 | /**
381 | * Reads next 16-bit value, LSB first.
382 | */
383 | private fun readShort(): Int {
384 | // Read 16-bit value.
385 | return rawData!!.short.toInt()
386 | }
387 |
388 | private fun err(): Boolean {
389 | return header!!.status != GifDecoder.STATUS_OK
390 | }
391 |
392 | companion object {
393 | const val TAG = "GifHeaderParser"
394 |
395 | // The minimum frame delay in hundredths of a second.
396 | const val MIN_FRAME_DELAY = 2
397 |
398 | // The default frame delay in hundredths of a second for GIFs with frame delays less than the
399 | // minimum.
400 | const val DEFAULT_FRAME_DELAY = 10
401 | private const val MAX_BLOCK_SIZE = 256
402 | }
403 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/felipecsl/gifimageview/library/GifDecoder.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Xcellent Creations, Inc.
3 | * Copyright 2014 Google, Inc. All rights reserved.
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6 | * associated documentation files (the "Software"), to deal in the Software without restriction,
7 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
8 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all copies or
12 | * substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 | */
20 | package com.felipecsl.gifimageview.library
21 |
22 | import android.annotation.TargetApi
23 | import android.graphics.Bitmap
24 | import android.os.Build
25 | import android.util.Log
26 | import java.io.ByteArrayOutputStream
27 | import java.io.IOException
28 | import java.io.InputStream
29 | import java.nio.ByteBuffer
30 | import java.nio.ByteOrder
31 | import java.util.*
32 |
33 | /**
34 | * Reads frame data from a GIF image source and decodes it into individual frames
35 | * for animation purposes. Image data can be read from either and InputStream source
36 | * or a byte[].
37 | *
38 | * This class is optimized for running animations with the frames, there
39 | * are no methods to get individual frame images, only to decode the next frame in the
40 | * animation sequence. Instead, it lowers its memory footprint by only housing the minimum
41 | * data necessary to decode the next frame in the animation sequence.
42 | *
43 | * The animation must be manually moved forward using [.advance] before requesting the next
44 | * frame. This method must also be called before you request the first frame or an error will
45 | * occur.
46 | *
47 | * Implementation adapted from sample code published in Lyons. (2004). *Java for Programmers*,
48 | * republished under the MIT Open Source License
49 | */
50 | internal class GifDecoder @JvmOverloads constructor(private val bitmapProvider: BitmapProvider = SimpleBitmapProvider()) {
51 | // Global File Header values and parsing flags.
52 | // Active color table.
53 | private var act: IntArray? = null
54 |
55 | // Private color table that can be modified if needed.
56 | private val pct = IntArray(256)
57 |
58 | // Raw GIF data from input source.
59 | var data: ByteBuffer? = null
60 | private set
61 |
62 | // Raw data read working array.
63 | private var block: ByteArray? = null
64 | private var workBuffer: ByteArray? = null
65 | private var workBufferSize = 0
66 | private var workBufferPosition = 0
67 | private var parser: GifHeaderParser? = null
68 |
69 | // LZW decoder working arrays.
70 | private var prefix: ShortArray? = null
71 | private var suffix: ByteArray? = null
72 | private var pixelStack: ByteArray? = null
73 | private var mainPixels: ByteArray? = null
74 | private var mainScratch: IntArray? = null
75 |
76 | /**
77 | * Gets the current index of the animation frame, or -1 if animation hasn't not yet started.
78 | *
79 | * @return frame index.
80 | */
81 | var currentFrameIndex = 0
82 | private set
83 |
84 | /**
85 | * Gets the number of loops that have been shown.
86 | *
87 | * @return iteration count.
88 | */
89 | var loopIndex = 0
90 | private set
91 | private var header: GifHeader?
92 | private var previousImage: Bitmap? = null
93 | private var savePrevious = false
94 |
95 | /**
96 | * Returns the current status of the decoder.
97 | *
98 | *
99 | * Status will update per frame to allow the caller to tell whether or not the current frame
100 | * was decoded successfully and/or completely. Format and open failures persist across frames.
101 | *
102 | */
103 | var status = 0
104 | private set
105 | private var sampleSize = 0
106 | private var downsampledHeight = 0
107 | private var downsampledWidth = 0
108 | private var isFirstFrameTransparent = false
109 |
110 | /**
111 | * An interface that can be used to provide reused [android.graphics.Bitmap]s to avoid GCs
112 | * from constantly allocating [android.graphics.Bitmap]s for every frame.
113 | */
114 | internal interface BitmapProvider {
115 | /**
116 | * Returns an [Bitmap] with exactly the given dimensions and config.
117 | *
118 | * @param width The width in pixels of the desired [android.graphics.Bitmap].
119 | * @param height The height in pixels of the desired [android.graphics.Bitmap].
120 | * @param config The [android.graphics.Bitmap.Config] of the desired [ ].
121 | */
122 | fun obtain(width: Int, height: Int, config: Bitmap.Config?): Bitmap
123 |
124 | /**
125 | * Releases the given Bitmap back to the pool.
126 | */
127 | fun release(bitmap: Bitmap?)
128 |
129 | /**
130 | * Returns a byte array used for decoding and generating the frame bitmap.
131 | *
132 | * @param size the size of the byte array to obtain
133 | */
134 | fun obtainByteArray(size: Int): ByteArray?
135 |
136 | /**
137 | * Releases the given byte array back to the pool.
138 | */
139 | fun release(bytes: ByteArray?)
140 |
141 | /**
142 | * Returns an int array used for decoding/generating the frame bitmaps.
143 | * @param size
144 | */
145 | fun obtainIntArray(size: Int): IntArray?
146 |
147 | /**
148 | * Release the given array back to the pool.
149 | * @param array
150 | */
151 | fun release(array: IntArray?)
152 | }
153 |
154 | @JvmOverloads
155 | constructor(
156 | provider: BitmapProvider, gifHeader: GifHeader?, rawData: ByteBuffer,
157 | sampleSize: Int = 1 /*sampleSize*/
158 | ) : this(provider) {
159 | setData(gifHeader, rawData, sampleSize)
160 | }
161 |
162 |
163 | fun getWidth(): Int? {
164 | return header?.width
165 | }
166 |
167 | fun getHeight(): Int? {
168 | return header?.height
169 | }
170 |
171 |
172 | /**
173 | * Move the animation frame counter forward.
174 | *
175 | * @return boolean specifying if animation should continue or if loopCount has been fulfilled.
176 | */
177 | fun advance(): Boolean {
178 | if (header!!.numFrames <= 0) {
179 | return false
180 | }
181 | if (currentFrameIndex == frameCount - 1) {
182 | loopIndex++
183 | }
184 | if (header!!.loopCount != LOOP_FOREVER && loopIndex > header!!.loopCount) {
185 | return false
186 | }
187 | currentFrameIndex = (currentFrameIndex + 1) % header!!.numFrames
188 | return true
189 | }
190 |
191 | /**
192 | * Gets display duration for specified frame.
193 | *
194 | * @param n int index of frame.
195 | * @return delay in milliseconds.
196 | */
197 | fun getDelay(n: Int): Int {
198 | var delay = -1
199 | if (n >= 0 && n < header!!.numFrames) {
200 | delay = header!!.frames[n].delay
201 | }
202 | return delay
203 | }
204 |
205 | /**
206 | * Gets display duration for the upcoming frame in ms.
207 | */
208 | val nextDelay: Int
209 | get() = if (header!!.numFrames <= 0 || currentFrameIndex < 0) {
210 | 0
211 | } else getDelay(currentFrameIndex)
212 |
213 | /**
214 | * Gets the number of frames read from file.
215 | *
216 | * @return frame count.
217 | */
218 | val frameCount: Int
219 | get() = header!!.numFrames
220 |
221 | /**
222 | * Sets the frame pointer to a specific frame
223 | *
224 | * @return boolean true if the move was successful
225 | */
226 | fun setFrameIndex(frame: Int): Boolean {
227 | if (frame < INITIAL_FRAME_POINTER || frame >= frameCount) {
228 | return false
229 | }
230 | currentFrameIndex = frame
231 | return true
232 | }
233 |
234 | /**
235 | * Resets the frame pointer to before the 0th frame, as if we'd never used this decoder to
236 | * decode any frames.
237 | */
238 | fun resetFrameIndex() {
239 | currentFrameIndex = INITIAL_FRAME_POINTER
240 | }
241 |
242 | /**
243 | * Resets the loop index to the first loop.
244 | */
245 | fun resetLoopIndex() {
246 | loopIndex = 0
247 | }
248 |
249 | /**
250 | * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitely.
251 | *
252 | * @return iteration count if one was specified, else 1.
253 | */
254 | val loopCount: Int
255 | get() = header!!.loopCount
256 |
257 | /**
258 | * Returns an estimated byte size for this decoder based on the data provided to [ ][.setData], as well as internal buffers.
259 | */
260 | val byteSize: Int
261 | get() = data!!.limit() + mainPixels!!.size + mainScratch!!.size * BYTES_PER_INTEGER// Prepare local copy of color table ("pct = act"), see #1068
262 | // Forget about act reference from shared header object, use copied version
263 | // Set transparent color if specified.
264 |
265 | // Transfer pixel data to image.
266 | // No color table defined.
267 |
268 | // Reset the transparent pixel in the color table
269 | // Set the appropriate color table.
270 | /**
271 | * Get the next frame in the animation sequence.
272 | *
273 | * @return Bitmap representation of frame.
274 | */
275 | @get:Synchronized
276 | val nextFrame: Bitmap?
277 | get() {
278 | if (header!!.numFrames <= 0 || currentFrameIndex < 0) {
279 | if (Log.isLoggable(TAG, Log.DEBUG)) {
280 | Log.d(
281 | TAG,
282 | "unable to decode frame, frameCount=" + header!!.numFrames + " framePointer="
283 | + currentFrameIndex
284 | )
285 | }
286 | status = STATUS_FORMAT_ERROR
287 | }
288 | if (status == STATUS_FORMAT_ERROR || status == STATUS_OPEN_ERROR) {
289 | if (Log.isLoggable(TAG, Log.DEBUG)) {
290 | Log.d(TAG, "Unable to decode frame, status=$status")
291 | }
292 | return null
293 | }
294 | status = STATUS_OK
295 | val currentFrame = header!!.frames[currentFrameIndex]
296 | var previousFrame: GifFrame? = null
297 | val previousIndex = currentFrameIndex - 1
298 | if (previousIndex >= 0) {
299 | previousFrame = header!!.frames[previousIndex]
300 | }
301 |
302 | // Set the appropriate color table.
303 | act = if (currentFrame.lct != null) currentFrame.lct else header!!.gct
304 | if (act == null) {
305 | if (Log.isLoggable(TAG, Log.DEBUG)) {
306 | Log.d(TAG, "No Valid Color Table for frame #$currentFrameIndex")
307 | }
308 | // No color table defined.
309 | status = STATUS_FORMAT_ERROR
310 | return null
311 | }
312 |
313 | // Reset the transparent pixel in the color table
314 | if (currentFrame.transparency) {
315 | // Prepare local copy of color table ("pct = act"), see #1068
316 | System.arraycopy(act, 0, pct, 0, act!!.size)
317 | // Forget about act reference from shared header object, use copied version
318 | act = pct
319 | // Set transparent color if specified.
320 | act!![currentFrame.transIndex] = 0
321 | }
322 |
323 | // Transfer pixel data to image.
324 | return setPixels(currentFrame, previousFrame)
325 | }
326 |
327 | /**
328 | * Reads GIF image from stream.
329 | *
330 | * @param is containing GIF file.
331 | * @return read status code (0 = no errors).
332 | */
333 | fun read(`is`: InputStream?, contentLength: Int): Int {
334 | if (`is` != null) {
335 | try {
336 | val capacity = if (contentLength > 0) contentLength + 4096 else 16384
337 | val buffer = ByteArrayOutputStream(capacity)
338 | var nRead: Int
339 | val data = ByteArray(16384)
340 | while (`is`.read(data, 0, data.size).also { nRead = it } != -1) {
341 | buffer.write(data, 0, nRead)
342 | }
343 | buffer.flush()
344 | read(buffer.toByteArray())
345 | } catch (e: IOException) {
346 | Log.w(TAG, "Error reading data from stream", e)
347 | }
348 | } else {
349 | status = STATUS_OPEN_ERROR
350 | }
351 | try {
352 | `is`?.close()
353 | } catch (e: IOException) {
354 | Log.w(TAG, "Error closing stream", e)
355 | }
356 | return status
357 | }
358 |
359 | fun clear() {
360 | header = null
361 | if (mainPixels != null) {
362 | bitmapProvider.release(mainPixels)
363 | }
364 | if (mainScratch != null) {
365 | bitmapProvider.release(mainScratch)
366 | }
367 | if (previousImage != null) {
368 | bitmapProvider.release(previousImage)
369 | }
370 | previousImage = null
371 | data = null
372 | isFirstFrameTransparent = false
373 | if (block != null) {
374 | bitmapProvider.release(block)
375 | }
376 | if (workBuffer != null) {
377 | bitmapProvider.release(workBuffer)
378 | }
379 | }
380 |
381 | @Synchronized
382 | fun setData(header: GifHeader?, data: ByteArray?) {
383 | setData(header, ByteBuffer.wrap(data))
384 | }
385 |
386 | @Synchronized
387 | fun setData(header: GifHeader?, buffer: ByteBuffer) {
388 | setData(header, buffer, 1)
389 | }
390 |
391 | @Synchronized
392 | fun setData(header: GifHeader?, buffer: ByteBuffer, sampleSize: Int) {
393 | var sampleSize = sampleSize
394 | require(sampleSize > 0) { "Sample size must be >=0, not: $sampleSize" }
395 | // Make sure sample size is a power of 2.
396 | sampleSize = Integer.highestOneBit(sampleSize)
397 | status = STATUS_OK
398 | this.header = header
399 | isFirstFrameTransparent = false
400 | currentFrameIndex = INITIAL_FRAME_POINTER
401 | resetLoopIndex()
402 | // Initialize the raw data buffer.
403 | data = buffer.asReadOnlyBuffer()
404 | data?.position(0)
405 | data?.order(ByteOrder.LITTLE_ENDIAN)
406 |
407 | // No point in specially saving an old frame if we're never going to use it.
408 | savePrevious = false
409 | for (frame in header!!.frames) {
410 | if (frame.dispose == DISPOSAL_PREVIOUS) {
411 | savePrevious = true
412 | break
413 | }
414 | }
415 | this.sampleSize = sampleSize
416 | downsampledWidth = header.width / sampleSize
417 | downsampledHeight = header.height / sampleSize
418 | // Now that we know the size, init scratch arrays.
419 | // TODO Find a way to avoid this entirely or at least downsample it (either should be possible).
420 | mainPixels = bitmapProvider.obtainByteArray(header.width * header.height)
421 | mainScratch = bitmapProvider.obtainIntArray(downsampledWidth * downsampledHeight)
422 | }
423 |
424 | private val headerParser: GifHeaderParser
425 | private get() {
426 | if (parser == null) {
427 | parser = GifHeaderParser()
428 | }
429 | return parser!!
430 | }
431 |
432 | /**
433 | * Reads GIF image from byte array.
434 | *
435 | * @param data containing GIF file.
436 | * @return read status code (0 = no errors).
437 | */
438 | @Synchronized
439 | fun read(data: ByteArray?): Int {
440 | header = headerParser.setData(data).parseHeader()
441 | data?.let { setData(header, it) }
442 | return status
443 | }
444 |
445 | /**
446 | * Creates new frame image from current data (and previous frames as specified by their
447 | * disposition codes).
448 | */
449 | private fun setPixels(currentFrame: GifFrame, previousFrame: GifFrame?): Bitmap {
450 | // Final location of blended pixels.
451 | val dest = mainScratch
452 |
453 | // clear all pixels when meet first frame
454 | if (previousFrame == null) {
455 | Arrays.fill(dest, 0)
456 | }
457 |
458 | // fill in starting image contents based on last image's dispose code
459 | if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) {
460 | // We don't need to do anything for DISPOSAL_NONE, if it has the correct pixels so will our
461 | // mainScratch and therefore so will our dest array.
462 | if (previousFrame.dispose == DISPOSAL_BACKGROUND) {
463 | // Start with a canvas filled with the background color
464 | var c = 0
465 | if (!currentFrame.transparency) {
466 | c = header!!.bgColor
467 | if (currentFrame.lct != null && header!!.bgIndex == currentFrame.transIndex) {
468 | c = 0
469 | }
470 | } else if (currentFrameIndex == 0) {
471 | // TODO: We should check and see if all individual pixels are replaced. If they are, the
472 | // first frame isn't actually transparent. For now, it's simpler and safer to assume
473 | // drawing a transparent background means the GIF contains transparency.
474 | isFirstFrameTransparent = true
475 | }
476 | fillRect(dest, previousFrame, c)
477 | } else if (previousFrame.dispose == DISPOSAL_PREVIOUS) {
478 | if (previousImage == null) {
479 | fillRect(dest, previousFrame, 0)
480 | } else {
481 | // Start with the previous frame
482 | val downsampledIH = previousFrame.ih / sampleSize
483 | val downsampledIY = previousFrame.iy / sampleSize
484 | val downsampledIW = previousFrame.iw / sampleSize
485 | val downsampledIX = previousFrame.ix / sampleSize
486 | val topLeft = downsampledIY * downsampledWidth + downsampledIX
487 | previousImage!!.getPixels(
488 | dest, topLeft, downsampledWidth,
489 | downsampledIX, downsampledIY, downsampledIW, downsampledIH
490 | )
491 | }
492 | }
493 | }
494 |
495 | // Decode pixels for this frame into the global pixels[] scratch.
496 | decodeBitmapData(currentFrame)
497 | val downsampledIH = currentFrame.ih / sampleSize
498 | val downsampledIY = currentFrame.iy / sampleSize
499 | val downsampledIW = currentFrame.iw / sampleSize
500 | val downsampledIX = currentFrame.ix / sampleSize
501 | // Copy each source line to the appropriate place in the destination.
502 | var pass = 1
503 | var inc = 8
504 | var iline = 0
505 | val isFirstFrame = currentFrameIndex == 0
506 | for (i in 0 until downsampledIH) {
507 | var line = i
508 | if (currentFrame.interlace) {
509 | if (iline >= downsampledIH) {
510 | pass++
511 | when (pass) {
512 | 2 -> iline = 4
513 | 3 -> {
514 | iline = 2
515 | inc = 4
516 | }
517 | 4 -> {
518 | iline = 1
519 | inc = 2
520 | }
521 | else -> {}
522 | }
523 | }
524 | line = iline
525 | iline += inc
526 | }
527 | line += downsampledIY
528 | if (line < downsampledHeight) {
529 | val k = line * downsampledWidth
530 | // Start of line in dest.
531 | var dx = k + downsampledIX
532 | // End of dest line.
533 | var dlim = dx + downsampledIW
534 | if (k + downsampledWidth < dlim) {
535 | // Past dest edge.
536 | dlim = k + downsampledWidth
537 | }
538 | // Start of line in source.
539 | var sx = i * sampleSize * currentFrame.iw
540 | val maxPositionInSource = sx + (dlim - dx) * sampleSize
541 | while (dx < dlim) {
542 | // Map color and insert in destination.
543 | var averageColor: Int
544 | averageColor = if (sampleSize == 1) {
545 | val currentColorIndex = mainPixels!![sx].toInt() and 0x000000ff
546 | act!![currentColorIndex]
547 | } else {
548 | // TODO: This is substantially slower (up to 50ms per frame) than just grabbing the
549 | // current color index above, even with a sample size of 1.
550 | averageColorsNear(sx, maxPositionInSource, currentFrame.iw)
551 | }
552 | if (averageColor != 0) {
553 | dest!![dx] = averageColor
554 | } else if (!isFirstFrameTransparent && isFirstFrame) {
555 | isFirstFrameTransparent = true
556 | }
557 | sx += sampleSize
558 | dx++
559 | }
560 | }
561 | }
562 |
563 | // Copy pixels into previous image
564 | if (savePrevious && (currentFrame.dispose == DISPOSAL_UNSPECIFIED
565 | || currentFrame.dispose == DISPOSAL_NONE)
566 | ) {
567 | if (previousImage == null) {
568 | previousImage = nextBitmap
569 | }
570 | previousImage!!.setPixels(
571 | dest, 0, downsampledWidth, 0, 0, downsampledWidth,
572 | downsampledHeight
573 | )
574 | }
575 |
576 | // Set pixels for current image.
577 | val result = nextBitmap
578 | result.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight)
579 | return result
580 | }
581 |
582 | private fun fillRect(dest: IntArray?, frame: GifFrame, bgColor: Int) {
583 | // The area used by the graphic must be restored to the background color.
584 | val downsampledIH = frame.ih / sampleSize
585 | val downsampledIY = frame.iy / sampleSize
586 | val downsampledIW = frame.iw / sampleSize
587 | val downsampledIX = frame.ix / sampleSize
588 | val topLeft = downsampledIY * downsampledWidth + downsampledIX
589 | val bottomLeft = topLeft + downsampledIH * downsampledWidth
590 | var left = topLeft
591 | while (left < bottomLeft) {
592 | val right = left + downsampledIW
593 | for (pointer in left until right) {
594 | dest!![pointer] = bgColor
595 | }
596 | left += downsampledWidth
597 | }
598 | }
599 |
600 | private fun averageColorsNear(
601 | positionInMainPixels: Int, maxPositionInMainPixels: Int,
602 | currentFrameIw: Int
603 | ): Int {
604 | var alphaSum = 0
605 | var redSum = 0
606 | var greenSum = 0
607 | var blueSum = 0
608 | var totalAdded = 0
609 | // Find the pixels in the current row.
610 | run {
611 | var i = positionInMainPixels
612 | while (i < positionInMainPixels + sampleSize && i < mainPixels!!.size && i < maxPositionInMainPixels) {
613 | val currentColorIndex = mainPixels!![i].toInt() and 0xff
614 | val currentColor = act!![currentColorIndex]
615 | if (currentColor != 0) {
616 | alphaSum += currentColor shr 24 and 0x000000ff
617 | redSum += currentColor shr 16 and 0x000000ff
618 | greenSum += currentColor shr 8 and 0x000000ff
619 | blueSum += currentColor and 0x000000ff
620 | totalAdded++
621 | }
622 | i++
623 | }
624 | }
625 | // Find the pixels in the next row.
626 | var i = positionInMainPixels + currentFrameIw
627 | while (i < positionInMainPixels + currentFrameIw + sampleSize && i < mainPixels!!.size && i < maxPositionInMainPixels) {
628 | val currentColorIndex = mainPixels!![i].toInt() and 0xff
629 | val currentColor = act!![currentColorIndex]
630 | if (currentColor != 0) {
631 | alphaSum += currentColor shr 24 and 0x000000ff
632 | redSum += currentColor shr 16 and 0x000000ff
633 | greenSum += currentColor shr 8 and 0x000000ff
634 | blueSum += currentColor and 0x000000ff
635 | totalAdded++
636 | }
637 | i++
638 | }
639 | return if (totalAdded == 0) {
640 | 0
641 | } else {
642 | (alphaSum / totalAdded shl 24
643 | or (redSum / totalAdded shl 16)
644 | or (greenSum / totalAdded shl 8)
645 | or blueSum / totalAdded)
646 | }
647 | }
648 |
649 | /**
650 | * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick.
651 | */
652 | private fun decodeBitmapData(frame: GifFrame?) {
653 | workBufferSize = 0
654 | workBufferPosition = 0
655 | if (frame != null) {
656 | // Jump to the frame start position.
657 | data!!.position(frame.bufferFrameStart)
658 | }
659 | val npix = if (frame == null) header!!.width * header!!.height else frame.iw * frame.ih
660 | var available: Int
661 | val clear: Int
662 | var codeMask: Int
663 | var codeSize: Int
664 | val endOfInformation: Int
665 | var inCode: Int
666 | var oldCode: Int
667 | var bits: Int
668 | var code: Int
669 | var count: Int
670 | var i: Int
671 | var datum: Int
672 | val dataSize: Int
673 | var first: Int
674 | var top: Int
675 | var bi: Int
676 | var pi: Int
677 | if (mainPixels == null || mainPixels!!.size < npix) {
678 | // Allocate new pixel array.
679 | mainPixels = bitmapProvider.obtainByteArray(npix)
680 | }
681 | if (prefix == null) {
682 | prefix = ShortArray(MAX_STACK_SIZE)
683 | }
684 | if (suffix == null) {
685 | suffix = ByteArray(MAX_STACK_SIZE)
686 | }
687 | if (pixelStack == null) {
688 | pixelStack = ByteArray(MAX_STACK_SIZE + 1)
689 | }
690 |
691 | // Initialize GIF data stream decoder.
692 | dataSize = readByte()
693 | clear = 1 shl dataSize
694 | endOfInformation = clear + 1
695 | available = clear + 2
696 | oldCode = NULL_CODE
697 | codeSize = dataSize + 1
698 | codeMask = (1 shl codeSize) - 1
699 | code = 0
700 | while (code < clear) {
701 |
702 | // XXX ArrayIndexOutOfBoundsException.
703 | prefix!![code] = 0
704 | suffix!![code] = code.toByte()
705 | code++
706 | }
707 |
708 | // Decode GIF pixel stream.
709 | bi = 0
710 | pi = bi
711 | top = pi
712 | first = top
713 | count = first
714 | bits = count
715 | datum = bits
716 | i = 0
717 | while (i < npix) {
718 |
719 | // Load bytes until there are enough bits for a code.
720 | if (count == 0) {
721 | // Read a new data block.
722 | count = readBlock()
723 | if (count <= 0) {
724 | status = STATUS_PARTIAL_DECODE
725 | break
726 | }
727 | bi = 0
728 | }
729 | datum += block!![bi].toInt() and 0xff shl bits
730 | bits += 8
731 | bi++
732 | count--
733 | while (bits >= codeSize) {
734 | // Get the next code.
735 | code = datum and codeMask
736 | datum = datum shr codeSize
737 | bits -= codeSize
738 |
739 | // Interpret the code.
740 | if (code == clear) {
741 | // Reset decoder.
742 | codeSize = dataSize + 1
743 | codeMask = (1 shl codeSize) - 1
744 | available = clear + 2
745 | oldCode = NULL_CODE
746 | continue
747 | }
748 | if (code > available) {
749 | status = STATUS_PARTIAL_DECODE
750 | break
751 | }
752 | if (code == endOfInformation) {
753 | break
754 | }
755 | if (oldCode == NULL_CODE) {
756 | pixelStack!![top++] = suffix!![code]
757 | oldCode = code
758 | first = code
759 | continue
760 | }
761 | inCode = code
762 | if (code >= available) {
763 | pixelStack!![top++] = first.toByte()
764 | code = oldCode
765 | }
766 | while (code >= clear) {
767 | pixelStack!![top++] = suffix!![code]
768 | code = prefix!![code].toInt()
769 | }
770 | first = suffix!![code].toInt() and 0xff
771 | pixelStack!![top++] = first.toByte()
772 |
773 | // Add a new string to the string table.
774 | if (available < MAX_STACK_SIZE) {
775 | prefix!![available] = oldCode.toShort()
776 | suffix!![available] = first.toByte()
777 | available++
778 | if (available and codeMask == 0 && available < MAX_STACK_SIZE) {
779 | codeSize++
780 | codeMask += available
781 | }
782 | }
783 | oldCode = inCode
784 | while (top > 0) {
785 | // Pop a pixel off the pixel stack.
786 | mainPixels!![pi++] = pixelStack!![--top]
787 | i++
788 | }
789 | }
790 | }
791 |
792 | // Clear missing pixels.
793 | i = pi
794 | while (i < npix) {
795 | mainPixels!![i] = 0
796 | i++
797 | }
798 | }
799 |
800 | /**
801 | * Reads the next chunk for the intermediate work buffer.
802 | */
803 | private fun readChunkIfNeeded() {
804 | if (workBufferSize > workBufferPosition) {
805 | return
806 | }
807 | if (workBuffer == null) {
808 | workBuffer = bitmapProvider.obtainByteArray(WORK_BUFFER_SIZE) //?: ByteArray(WORK_BUFFER_SIZE)
809 | }
810 | workBufferPosition = 0
811 | workBufferSize = Math.min(data!!.remaining(), WORK_BUFFER_SIZE)
812 | data!![workBuffer, 0, workBufferSize]
813 | }
814 |
815 | /**
816 | * Reads a single byte from the input stream.
817 | */
818 | private fun readByte(): Int {
819 | return try {
820 | readChunkIfNeeded()
821 | workBuffer!![workBufferPosition++].toInt() and 0xFF
822 | // ByteArray(10)[3]and 0xFF
823 | } catch (e: Exception) {
824 | status = STATUS_FORMAT_ERROR
825 | 0
826 | }
827 | }
828 |
829 | /**
830 | * Reads next variable length block from input.
831 | *
832 | * @return number of bytes stored in "buffer".
833 | */
834 | private fun readBlock(): Int {
835 | val blockSize = readByte()
836 | if (blockSize > 0) {
837 | try {
838 | if (block == null) {
839 | block = bitmapProvider.obtainByteArray(255)
840 | }
841 | val remaining = workBufferSize - workBufferPosition
842 | if (remaining >= blockSize) {
843 | // Block can be read from the current work buffer.
844 | System.arraycopy(workBuffer, workBufferPosition, block, 0, blockSize)
845 | workBufferPosition += blockSize
846 | } else if (data!!.remaining() + remaining >= blockSize) {
847 | // Block can be read in two passes.
848 | System.arraycopy(workBuffer, workBufferPosition, block, 0, remaining)
849 | workBufferPosition = workBufferSize
850 | readChunkIfNeeded()
851 | val secondHalfRemaining = blockSize - remaining
852 | System.arraycopy(workBuffer, 0, block, remaining, secondHalfRemaining)
853 | workBufferPosition += secondHalfRemaining
854 | } else {
855 | status = STATUS_FORMAT_ERROR
856 | }
857 | } catch (e: Exception) {
858 | Log.w(TAG, "Error Reading Block", e)
859 | status = STATUS_FORMAT_ERROR
860 | }
861 | }
862 | return blockSize
863 | }
864 |
865 | private val nextBitmap: Bitmap
866 | private get() {
867 | val config =
868 | if (isFirstFrameTransparent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
869 | val result = bitmapProvider.obtain(downsampledWidth, downsampledHeight, config)
870 | setAlpha(result)
871 | return result
872 | }
873 |
874 | companion object {
875 | private val TAG = GifDecoder::class.java.simpleName
876 |
877 | /**
878 | * File read status: No errors.
879 | */
880 | const val STATUS_OK = 0
881 |
882 | /**
883 | * File read status: Error decoding file (may be partially decoded).
884 | */
885 | const val STATUS_FORMAT_ERROR = 1
886 |
887 | /**
888 | * File read status: Unable to open source.
889 | */
890 | const val STATUS_OPEN_ERROR = 2
891 |
892 | /**
893 | * Unable to fully decode the current frame.
894 | */
895 | const val STATUS_PARTIAL_DECODE = 3
896 |
897 | /**
898 | * max decoder pixel stack size.
899 | */
900 | private const val MAX_STACK_SIZE = 4096
901 |
902 | /**
903 | * GIF Disposal Method meaning take no action.
904 | */
905 | private const val DISPOSAL_UNSPECIFIED = 0
906 |
907 | /**
908 | * GIF Disposal Method meaning leave canvas from previous frame.
909 | */
910 | private const val DISPOSAL_NONE = 1
911 |
912 | /**
913 | * GIF Disposal Method meaning clear canvas to background color.
914 | */
915 | private const val DISPOSAL_BACKGROUND = 2
916 |
917 | /**
918 | * GIF Disposal Method meaning clear canvas to frame before last.
919 | */
920 | private const val DISPOSAL_PREVIOUS = 3
921 | private const val NULL_CODE = -1
922 | private const val INITIAL_FRAME_POINTER = -1
923 | const val LOOP_FOREVER = -1
924 | private const val BYTES_PER_INTEGER = 4
925 |
926 | // Temporary buffer for block reading. Reads 16k chunks from the native buffer for processing,
927 | // to greatly reduce JNI overhead.
928 | private const val WORK_BUFFER_SIZE = 16384
929 | @TargetApi(12)
930 | private fun setAlpha(bitmap: Bitmap) {
931 | if (Build.VERSION.SDK_INT >= 12) {
932 | bitmap.setHasAlpha(true)
933 | }
934 | }
935 | }
936 |
937 | init {
938 | header = GifHeader()
939 | }
940 | }
--------------------------------------------------------------------------------