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)