From 0a182b419492dce693409462328e548624840a6b Mon Sep 17 00:00:00 2001 From: Richard Kuiper Date: Tue, 6 Nov 2018 14:53:17 +0000 Subject: [PATCH] Added duration and loop completed callback Added CustomAnimationDrawable that provides listener for callbacks. Added the duration for each frame which can override the png value Refactored some of the ApngAnimator to reuse code --- .../src/main/java/oupson/apng/ApngAnimator.kt | 259 ++++++++++-------- .../oupson/apng/CustomAnimationDrawable.kt | 38 +++ .../oupson/apng/exceptions/customException.kt | 8 +- .../java/oupson/apngcreator/Main2Activity.kt | 14 +- .../java/oupson/apngcreator/MainActivity.kt | 18 +- 5 files changed, 209 insertions(+), 128 deletions(-) create mode 100644 apng_library/src/main/java/oupson/apng/CustomAnimationDrawable.kt 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)