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
This commit is contained in:
Richard Kuiper 2018-11-06 14:53:17 +00:00
parent da8753e2c5
commit 0a182b4194
No known key found for this signature in database
GPG Key ID: 4284CF11524E9190
5 changed files with 209 additions and 128 deletions

View File

@ -2,86 +2,160 @@ package oupson.apng
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.*
import android.graphics.drawable.AnimationDrawable
import android.os.Handler import android.os.Handler
import android.widget.ImageView import android.widget.ImageView
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
import oupson.apng.exceptions.NotApngException
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import android.graphics.drawable.Drawable
/** /**
* Class to play APNG * Class to play APNG
*/ */
class ApngAnimator(val context: Context) { class ApngAnimator {
var isPlaying = true var isPlaying = true
private set(value) {field = value} private set(value) {
field = value
}
var Frames = ArrayList<Frame>() private var frames = ArrayList<Frame>()
private var myHandler: Handler = Handler()
private var counter = 0
private val generatedFrame = ArrayList<Bitmap>()
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<Bitmap>()
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 * Load into an imageview
* @param imageView Image view selected. * @param imageView Image view selected.
*/ */
fun loadInto(imageView: ImageView) : ApngAnimator { fun loadInto(imageView: ImageView): ApngAnimator {
this.imageView = imageView this.imageView = imageView
return this return this
} }
/** /**
* Load an APNG file * Load an APNG file and starts playing the animation.
* @param file The file to load * @param file The file to load
* @throws NotApngException * @throws NotApngException
*/ */
fun load(file: File) { fun load(file: File, frameDuration: Int? = null, animationListener: AnimationListener? = null) {
doAsync { doAsync {
// Download PNG // Download PNG
val extractedFrame = APNGDisassembler(file.readBytes()).pngList APNGDisassembler(file.readBytes()).pngList.apply {
draw(extractedFrame) draw(this)
anim = toAnimationDrawable() }
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 activeAnimation = anim
uiThread { uiThread {
imageView?.setImageBitmap(generatedFrame[0]) imageView?.apply {
imageView?.setImageDrawable(activeAnimation) setImageBitmap(generatedFrame[0])
setImageDrawable(activeAnimation)
}
activeAnimation?.start() 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 * Draw frames
*/ */
private fun draw(extractedFrame : ArrayList<Frame>) { private fun draw(extractedFrame: ArrayList<Frame>) {
// Set last frame // Set last frame
Frames = extractedFrame frames = extractedFrame
bitmapBuffer = Bitmap.createBitmap(Frames[0].maxWidth!!, Frames[0].maxHeight!!, Bitmap.Config.ARGB_8888) bitmapBuffer = Bitmap.createBitmap(frames[0].maxWidth!!, frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
for (i in 0 until Frames.size) { for (i in 0 until frames.size) {
// Iterator // Iterator
val it = Frames[i] val it = frames[i]
// Current bitmap for the frame // 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 canvas = Canvas(btm)
val current = BitmapFactory.decodeByteArray(it.byteArray, 0, it.byteArray.size).copy(Bitmap.Config.ARGB_8888, true) val current = BitmapFactory.decodeByteArray(it.byteArray, 0, it.byteArray.size).copy(Bitmap.Config.ARGB_8888, true)
// Write buffer to canvas // 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) canvas.drawBitmap(current, it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), null)
generatedFrame.add(btm) generatedFrame.add(btm)
// Don't add current frame to bitmap buffer // 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 //Do nothings
} }
// Add current frame to bitmap buffer // 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. // 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) { 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) val can = Canvas(res)
can.drawBitmap(btm, 0f, 0f, null) 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 }()) 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() { fun pause() {
isPlaying = false isPlaying = false
val animResume = AnimationDrawable() val animResume = CustomAnimationDrawable()
activeAnimation?.stop() activeAnimation?.stop()
val currentFrame = activeAnimation!!.current val currentFrame = activeAnimation!!.current
@ -195,25 +213,42 @@ class ApngAnimator(val context: Context) {
} }
} }
} }
fun play() { fun play() {
isPlaying = true 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 { private fun toAnimationDrawable(animationListener: CustomAnimationDrawable.AnimationListener? = null,
val animDrawable = AnimationDrawable() frameDuration: Int? = null): CustomAnimationDrawable {
for (i in 0 until generatedFrame.size) {
animDrawable.addFrame(BitmapDrawable(generatedFrame[i]), Frames[i].delay.toInt()) 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()
} }
} }

View File

@ -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
}
}

View File

@ -1,6 +1,6 @@
package oupson.apng.exceptions package oupson.apng.exceptions
class NoFrameException() : Exception() class NoFrameException : Exception()
class NotPngException() : Exception() class NotPngException : Exception()
class NotApngException() : Exception() class NotApngException : Exception()
class NoFcTL() : Exception() class NoFcTL : Exception()

View File

@ -46,24 +46,24 @@ class Main2Activity : AppCompatActivity() {
} }
fun load() { fun load() {
val animator = ApngAnimator(this).loadInto(imageView3) val animator = ApngAnimator().loadInto(imageView3)
val uri = intent.data val uri = intent.data
if (uri.toString().contains("file:///")) { if (uri.toString().contains("file:///")) {
try { try {
if (isApng(File(uri.path).readBytes())) { if (isApng(File(uri.path).readBytes())) {
animator.load(uri.path) animator.load(this, uri.path)
} else { } else {
imageView3.setImageBitmap(BitmapFactory.decodeFile(uri.path)) imageView3.setImageBitmap(BitmapFactory.decodeFile(uri.path))
Snackbar.make(constraint, "Not an APNG, and verified !", Snackbar.LENGTH_LONG).show() Snackbar.make(constraint, "Not an APNG, and verified !", Snackbar.LENGTH_LONG).show()
} }
} catch (e : NotApngException) { } catch (e: NotApngException) {
imageView3.setImageBitmap(BitmapFactory.decodeFile(uri.path)) imageView3.setImageBitmap(BitmapFactory.decodeFile(uri.path))
Snackbar.make(constraint, "Not an APNG", Snackbar.LENGTH_LONG).show() Snackbar.make(constraint, "Not an APNG", Snackbar.LENGTH_LONG).show()
} }
} else { } else {
try { try {
animator.load(getImageRealPath(contentResolver, uri, null)) animator.load(this, getImageRealPath(contentResolver, uri, null))
} catch (e : NotApngException) { } catch (e: NotApngException) {
imageView3.setImageBitmap(BitmapFactory.decodeFile(getImageRealPath(contentResolver, uri, null))) imageView3.setImageBitmap(BitmapFactory.decodeFile(getImageRealPath(contentResolver, uri, null)))
Snackbar.make(constraint, "Not an APNG", Snackbar.LENGTH_LONG).show() Snackbar.make(constraint, "Not an APNG", Snackbar.LENGTH_LONG).show()
} }
@ -75,7 +75,7 @@ class Main2Activity : AppCompatActivity() {
} else { } else {
animator.play() animator.play()
} }
} catch (e : Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }

View File

@ -2,22 +2,30 @@ package oupson.apngcreator
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.util.Log
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import oupson.apng.ApngAnimator import oupson.apng.ApngAnimator
class MainActivity : AppCompatActivity() { 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://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://raw.githubusercontent.com/tinify/iMessage-Panda-sticker/master/StickerPackExtension/Stickers.xcstickers/Sticker%20Pack.stickerpack/panda.sticker/panda.png"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
animator = ApngAnimator(this).loadInto(imageView) val animationListener = object : ApngAnimator.AnimationListener {
animator.load(imageUrl) 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) Picasso.get().load(imageUrl).into(imageView2)