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,45 +2,37 @@ 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<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
* @param imageView Image view selected.
@ -51,37 +43,119 @@ class ApngAnimator(val context: Context) {
}
/**
* Load an APNG file
* 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<Frame>) {
// 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()
private fun toAnimationDrawable(animationListener: CustomAnimationDrawable.AnimationListener? = null,
frameDuration: Int? = null): CustomAnimationDrawable {
return CustomAnimationDrawable().apply {
for (i in 0 until generatedFrame.size) {
animDrawable.addFrame(BitmapDrawable(generatedFrame[i]), Frames[i].delay.toInt())
}
return animDrawable
addFrame(BitmapDrawable(generatedFrame[i]), frameDuration
?: frames[i].delay.toInt())
}
fun setFrameSpeed(speed : Int) {
this.speed = speed
animationListener?.let { listener ->
this.setAnimationListener(listener)
}
}
}
/**
* 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
class NoFrameException() : Exception()
class NotPngException() : Exception()
class NotApngException() : Exception()
class NoFcTL() : Exception()
class NoFrameException : Exception()
class NotPngException : Exception()
class NotApngException : Exception()
class NoFcTL : Exception()

View File

@ -46,12 +46,12 @@ 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 {
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()
@ -62,7 +62,7 @@ class Main2Activity : AppCompatActivity() {
}
} else {
try {
animator.load(getImageRealPath(contentResolver, uri, null))
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()

View File

@ -2,6 +2,7 @@ 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
@ -10,14 +11,21 @@ import oupson.apng.ApngAnimator
class MainActivity : AppCompatActivity() {
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)