Merge pull request #4 from carkchard/master

Added duration and loop completed callback
This commit is contained in:
oupson 2018-11-09 18:36:41 +01:00 committed by GitHub
commit 2cf1095fdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 129 deletions

View File

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

View File

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

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,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()
}
}

View File

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