diff --git a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt
index 5ad0a54..ee279a4 100644
--- a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt
+++ b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt
@@ -2,86 +2,160 @@ package oupson.apng
import android.content.Context
import android.graphics.*
-import android.graphics.drawable.AnimationDrawable
import android.os.Handler
import android.widget.ImageView
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
+import oupson.apng.exceptions.NotApngException
import java.io.File
import java.net.URL
-import android.graphics.drawable.Drawable
-
-
/**
* Class to play APNG
*/
-class ApngAnimator(val context: Context) {
+class ApngAnimator {
var isPlaying = true
- private set(value) {field = value}
+ private set(value) {
+ field = value
+ }
- var Frames = ArrayList()
+ private var frames = ArrayList()
+ private var myHandler: Handler = Handler()
+ private var counter = 0
+ private val generatedFrame = ArrayList()
+ private var speed: Int? = null
+ private var lastFrame: Frame? = null
+ private var bitmapBuffer: Bitmap? = null
+ private var background: Bitmap? = null
+ private var imageView: ImageView? = null
+ private var anim: CustomAnimationDrawable? = null
+ private var activeAnimation: CustomAnimationDrawable? = null
+ private var currentDrawable = 0
+ private var animationLoopListener: AnimationListener? = null
- var myHandler: Handler = Handler()
-
- var counter = 0
-
- val generatedFrame = ArrayList()
-
- var speed = 1
-
- var lastFrame : Frame? = null
- var bitmapBuffer : Bitmap? = null
-
- var background : Bitmap? = null
-
- var imageView : ImageView? = null
-
- var anim : AnimationDrawable? = null
- var activeAnimation : AnimationDrawable? = null
-
- var currentDrawable = 0
/**
* Load into an imageview
* @param imageView Image view selected.
*/
- fun loadInto(imageView: ImageView) : ApngAnimator {
+ fun loadInto(imageView: ImageView): ApngAnimator {
this.imageView = imageView
return this
}
/**
- * Load an APNG file
- * @param file The file to load
+ * Load an APNG file and starts playing the animation.
+ * @param file The file to load
* @throws NotApngException
- */
- fun load(file: File) {
+ */
+ fun load(file: File, frameDuration: Int? = null, animationListener: AnimationListener? = null) {
doAsync {
// Download PNG
- val extractedFrame = APNGDisassembler(file.readBytes()).pngList
- draw(extractedFrame)
- anim = toAnimationDrawable()
+ APNGDisassembler(file.readBytes()).pngList.apply {
+ draw(this)
+ }
+
+ setupAnimationDrawableAndStart(frameDuration, animationListener)
+ }
+ }
+
+ /**
+ * Load an APNG file and starts playing the animation.
+ * @param context The current context.
+ * @param url URL to load.
+ * @param animationListener The listener that will be invoked when there are specific animation events.
+ * @param frameDuration The duration to show each frame. If this is null then the duration specified
+ * in the APNG will be used instead.
+ * @throws NotApngException
+ */
+ fun loadUrl(context: Context, url: URL, frameDuration: Int? = null, animationListener: AnimationListener? = null) {
+ doAsync(exceptionHandler = { e -> e.printStackTrace() }) {
+ // Download PNG
+ APNGDisassembler(Loader().load(context, url)).pngList.apply {
+ draw(this)
+ }
+
+ setupAnimationDrawableAndStart(frameDuration, animationListener)
+ }
+ }
+
+
+ /**
+ * Load an APNG file and starts playing the animation.
+ * @param byteArray ByteArray of the file
+ * @param animationListener The listener that will be invoked when there are specific animation events.
+ * @param frameDuration The duration to show each frame. If this is null then the duration specified
+ * in the APNG will be used instead.
+ * @throws NotApngException
+ */
+ fun load(byteArray: ByteArray, frameDuration: Int? = null, animationListener: AnimationListener? = null) {
+ doAsync {
+ APNGDisassembler(byteArray).pngList.apply {
+ draw(this)
+ }
+
+ setupAnimationDrawableAndStart(frameDuration, animationListener)
+ }
+ }
+
+ /**
+ * Sets up the animation drawable and any required listeners. The animation will automatically start.
+ * @param animationListener The listener that will be invoked when there are specific animation events.
+ * @param frameDuration The duration to show each frame. If this is null then the duration specified
+ * in the APNG will be used instead.
+ */
+ private fun setupAnimationDrawableAndStart(frameDuration: Int? = null, animationListener: AnimationListener? = null) {
+ doAsync {
+ var innerAnimationListener: CustomAnimationDrawable.AnimationListener? = null
+ animationListener?.apply {
+ innerAnimationListener = object : CustomAnimationDrawable.AnimationListener {
+ override fun onAnimationLooped() {
+ animationListener.onAnimationLooped()
+ }
+ }
+ }
+
+ anim = toAnimationDrawable(innerAnimationListener, frameDuration)
activeAnimation = anim
uiThread {
- imageView?.setImageBitmap(generatedFrame[0])
- imageView?.setImageDrawable(activeAnimation)
+ imageView?.apply {
+ setImageBitmap(generatedFrame[0])
+ setImageDrawable(activeAnimation)
+ }
activeAnimation?.start()
}
}
}
+ /**
+ * Load an APNG file
+ * @param context The current context.
+ * @param string Path of the file.
+ * @param animationListener The listener that will be invoked when there are specific animation events.
+ * @param frameDuration The duration to show each frame. If this is null then the duration specified
+ * in the APNG will be used instead.
+ * @throws NotApngException
+ */
+ fun load(context: Context, string: String, frameDuration: Int? = null, animationListener: AnimationListener? = null) {
+ if (string.contains("http") || string.contains("https")) {
+ val url = URL(string)
+ loadUrl(context, url, frameDuration, animationListener)
+ } else if (File(string).exists()) {
+ load(File(string), frameDuration, animationListener)
+ }
+ }
+
/**
* Draw frames
*/
- private fun draw(extractedFrame : ArrayList) {
+ private fun draw(extractedFrame: ArrayList) {
// Set last frame
- Frames = extractedFrame
- bitmapBuffer = Bitmap.createBitmap(Frames[0].maxWidth!!, Frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
- for (i in 0 until Frames.size) {
+ frames = extractedFrame
+ bitmapBuffer = Bitmap.createBitmap(frames[0].maxWidth!!, frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
+ for (i in 0 until frames.size) {
// Iterator
- val it = Frames[i]
+ val it = frames[i]
// Current bitmap for the frame
- val btm = Bitmap.createBitmap(Frames[0].maxWidth!!, Frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
+ val btm = Bitmap.createBitmap(frames[0].maxWidth!!, frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
val canvas = Canvas(btm)
val current = BitmapFactory.decodeByteArray(it.byteArray, 0, it.byteArray.size).copy(Bitmap.Config.ARGB_8888, true)
// Write buffer to canvas
@@ -95,13 +169,13 @@ class ApngAnimator(val context: Context) {
canvas.drawBitmap(current, it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), null)
generatedFrame.add(btm)
// Don't add current frame to bitmap buffer
- if (Frames[i].dispose_op == Utils.Companion.dispose_op.APNG_DISPOSE_OP_PREVIOUS) {
+ if (frames[i].dispose_op == Utils.Companion.dispose_op.APNG_DISPOSE_OP_PREVIOUS) {
//Do nothings
}
// Add current frame to bitmap buffer
// APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
else if (it.dispose_op == Utils.Companion.dispose_op.APNG_DISPOSE_OP_BACKGROUND) {
- val res = Bitmap.createBitmap(Frames[0].maxWidth!!, Frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
+ val res = Bitmap.createBitmap(frames[0].maxWidth!!, frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
val can = Canvas(res)
can.drawBitmap(btm, 0f, 0f, null)
can.drawRect(it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), it.x_offsets!! + it.width.toFloat(), it.y_offsets!! + it.height.toFloat(), { val paint = Paint(); paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR); paint }())
@@ -113,65 +187,9 @@ class ApngAnimator(val context: Context) {
}
}
- /**
- * Load an APNG file
- * @param url URL to load
- * @throws NotApngException
- */
- fun loadUrl(url : URL) {
- doAsync(exceptionHandler = {e -> e.printStackTrace()}) {
- // Download PNG
- val extractedFrame = APNGDisassembler(Loader().load(context, url)).pngList
- draw(extractedFrame)
- anim = toAnimationDrawable()
- activeAnimation = anim
- uiThread {
- imageView?.setImageBitmap(generatedFrame[0])
- imageView?.setImageDrawable(activeAnimation)
-
- activeAnimation?.start()
- }
- }
- }
-
- /**
- * Load an APNG file
- * @param byteArray ByteArray of the file
- * @throws NotApngException
- */
- fun load(byteArray: ByteArray) {
- doAsync {
- // Download PNG
- val extractedFrame = APNGDisassembler(byteArray).pngList
- draw(extractedFrame)
- anim = toAnimationDrawable()
- activeAnimation = anim
- uiThread {
- imageView?.setImageBitmap(generatedFrame[0])
- imageView?.setImageDrawable(activeAnimation)
- activeAnimation?.start()
- }
- }
-
- }
-
- /**
- * Load an APNG file
- * @param string Path of the file
- * @throws NotApngException
- */
- fun load(string: String) {
- if (string.contains("http") || string.contains("https")) {
- val url = URL(string)
- loadUrl(url)
- } else if (File(string).exists()) {
- load(File(string))
- }
- }
-
fun pause() {
isPlaying = false
- val animResume = AnimationDrawable()
+ val animResume = CustomAnimationDrawable()
activeAnimation?.stop()
val currentFrame = activeAnimation!!.current
@@ -195,25 +213,42 @@ class ApngAnimator(val context: Context) {
}
}
}
+
fun play() {
isPlaying = true
- activeAnimation?.start();
+ activeAnimation?.start()
}
-
-
/**
- * Return animation drawable of the APNG
+ * Converts the generated frames into an animation drawable ([CustomAnimationDrawable])
+ *
+ * @param animationListener The listener that will be invoked when there are specific animation events.
+ * @param frameDuration The duration to show each frame. If this is null then the duration specified
+ * in the APNG will be used instead.
*/
- fun toAnimationDrawable() : AnimationDrawable {
- val animDrawable = AnimationDrawable()
- for (i in 0 until generatedFrame.size) {
- animDrawable.addFrame(BitmapDrawable(generatedFrame[i]), Frames[i].delay.toInt())
+ private fun toAnimationDrawable(animationListener: CustomAnimationDrawable.AnimationListener? = null,
+ frameDuration: Int? = null): CustomAnimationDrawable {
+
+ return CustomAnimationDrawable().apply {
+ for (i in 0 until generatedFrame.size) {
+ addFrame(BitmapDrawable(generatedFrame[i]), frameDuration
+ ?: frames[i].delay.toInt())
+ }
+
+ animationListener?.let { listener ->
+ this.setAnimationListener(listener)
+ }
}
- return animDrawable
}
- fun setFrameSpeed(speed : Int) {
- this.speed = speed
+ /**
+ * Interface that exposes callbacks for events during the animation.
+ */
+ interface AnimationListener {
+
+ /**
+ * The animation has performed a loop.
+ */
+ fun onAnimationLooped()
}
}
\ No newline at end of file
diff --git a/apng_library/src/main/java/oupson/apng/CustomAnimationDrawable.kt b/apng_library/src/main/java/oupson/apng/CustomAnimationDrawable.kt
new file mode 100644
index 0000000..8ec30ef
--- /dev/null
+++ b/apng_library/src/main/java/oupson/apng/CustomAnimationDrawable.kt
@@ -0,0 +1,38 @@
+package oupson.apng
+
+import android.graphics.drawable.AnimationDrawable
+import oupson.apng.CustomAnimationDrawable.AnimationListener
+
+/**
+ * Extension of the [AnimationDrawable] that provides an [AnimationListener]. This will allow
+ * for the caller to listen for specific animation related events.
+ */
+internal class CustomAnimationDrawable : AnimationDrawable() {
+
+ /**
+ * Interface that exposes callbacks for events during the animation.
+ */
+ interface AnimationListener {
+
+ /**
+ * The animation has performed a loop.
+ */
+ fun onAnimationLooped()
+ }
+
+ private var animationListener: AnimationListener? = null
+
+ fun setAnimationListener(animationListener: AnimationListener) {
+ this.animationListener = animationListener
+ }
+
+ override fun selectDrawable(index: Int): Boolean {
+ val drawableChanged = super.selectDrawable(index)
+
+ if (index != 0 && index == numberOfFrames - 1) {
+ animationListener?.onAnimationLooped()
+ }
+
+ return drawableChanged
+ }
+}
\ No newline at end of file
diff --git a/apng_library/src/main/java/oupson/apng/exceptions/customException.kt b/apng_library/src/main/java/oupson/apng/exceptions/customException.kt
index 9134f2c..ba957c1 100644
--- a/apng_library/src/main/java/oupson/apng/exceptions/customException.kt
+++ b/apng_library/src/main/java/oupson/apng/exceptions/customException.kt
@@ -1,6 +1,6 @@
package oupson.apng.exceptions
-class NoFrameException() : Exception()
-class NotPngException() : Exception()
-class NotApngException() : Exception()
-class NoFcTL() : Exception()
\ No newline at end of file
+class NoFrameException : Exception()
+class NotPngException : Exception()
+class NotApngException : Exception()
+class NoFcTL : Exception()
\ No newline at end of file
diff --git a/app-test/src/main/java/oupson/apngcreator/Main2Activity.kt b/app-test/src/main/java/oupson/apngcreator/Main2Activity.kt
index e883861..c55ef5f 100644
--- a/app-test/src/main/java/oupson/apngcreator/Main2Activity.kt
+++ b/app-test/src/main/java/oupson/apngcreator/Main2Activity.kt
@@ -46,24 +46,24 @@ class Main2Activity : AppCompatActivity() {
}
fun load() {
- val animator = ApngAnimator(this).loadInto(imageView3)
+ val animator = ApngAnimator().loadInto(imageView3)
val uri = intent.data
if (uri.toString().contains("file:///")) {
- try {
+ try {
if (isApng(File(uri.path).readBytes())) {
- animator.load(uri.path)
+ animator.load(this, uri.path)
} else {
imageView3.setImageBitmap(BitmapFactory.decodeFile(uri.path))
Snackbar.make(constraint, "Not an APNG, and verified !", Snackbar.LENGTH_LONG).show()
}
- } catch (e : NotApngException) {
+ } catch (e: NotApngException) {
imageView3.setImageBitmap(BitmapFactory.decodeFile(uri.path))
Snackbar.make(constraint, "Not an APNG", Snackbar.LENGTH_LONG).show()
}
} else {
try {
- animator.load(getImageRealPath(contentResolver, uri, null))
- } catch (e : NotApngException) {
+ animator.load(this, getImageRealPath(contentResolver, uri, null))
+ } catch (e: NotApngException) {
imageView3.setImageBitmap(BitmapFactory.decodeFile(getImageRealPath(contentResolver, uri, null)))
Snackbar.make(constraint, "Not an APNG", Snackbar.LENGTH_LONG).show()
}
@@ -75,7 +75,7 @@ class Main2Activity : AppCompatActivity() {
} else {
animator.play()
}
- } catch (e : Exception) {
+ } catch (e: Exception) {
e.printStackTrace()
}
}
diff --git a/app-test/src/main/java/oupson/apngcreator/MainActivity.kt b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt
index 969a38b..3aeea34 100644
--- a/app-test/src/main/java/oupson/apngcreator/MainActivity.kt
+++ b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt
@@ -2,22 +2,30 @@ package oupson.apngcreator
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
+import android.util.Log
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_main.*
import oupson.apng.ApngAnimator
class MainActivity : AppCompatActivity() {
- lateinit var animator : ApngAnimator
+ lateinit var animator: ApngAnimator
- val imageUrl = "https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"
- //val imageUrl = "https://raw.githubusercontent.com/tinify/iMessage-Panda-sticker/master/StickerPackExtension/Stickers.xcstickers/Sticker%20Pack.stickerpack/panda.sticker/panda.png"
+ //val imageUrl = "https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"
+ val imageUrl = "https://raw.githubusercontent.com/tinify/iMessage-Panda-sticker/master/StickerPackExtension/Stickers.xcstickers/Sticker%20Pack.stickerpack/panda.sticker/panda.png"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
- animator = ApngAnimator(this).loadInto(imageView)
- animator.load(imageUrl)
+ val animationListener = object : ApngAnimator.AnimationListener {
+ override fun onAnimationLooped() {
+ Log.d("TEST", "Animation LOOPED!")
+ }
+ }
+
+ animator = ApngAnimator().loadInto(imageView).apply {
+ load(this@MainActivity, imageUrl, null, animationListener)
+ }
Picasso.get().load(imageUrl).into(imageView2)