Merge pull request #4 from carkchard/master
Added duration and loop completed callback
This commit is contained in:
commit
2cf1095fdf
|
@ -7,7 +7,7 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 26
|
||||
versionCode 1
|
||||
versionName "1.0.2"
|
||||
versionName "1.0.3"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
|
|
@ -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<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.
|
||||
*/
|
||||
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<Frame>) {
|
||||
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()
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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,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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue