├── 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 | 5 | 6 | 9 | 10 | 11 | 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 | [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-GifImageView-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/1339) 6 | [![Build Status](https://travis-ci.org/felipecsl/GifImageView.svg?branch=master)](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 | ![](https://raw.githubusercontent.com/felipecsl/GifImageView/master/demo.gif) 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 | } --------------------------------------------------------------------------------