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:
parent
da8753e2c5
commit
0a182b4194
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue