Merge branch 'coroutine'
This commit is contained in:
commit
11a5ba6005
|
@ -1,6 +1,5 @@
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -29,8 +28,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest package="oupson.apng">
|
<manifest package="oupson.apng">
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -3,23 +3,19 @@ package oupson.apng.decoder
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.*
|
import android.graphics.*
|
||||||
import android.graphics.drawable.AnimatedImageDrawable
|
import android.graphics.drawable.AnimatedImageDrawable
|
||||||
import android.graphics.drawable.AnimationDrawable
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.annotation.RawRes
|
import androidx.annotation.RawRes
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import oupson.apng.BuildConfig
|
import oupson.apng.BuildConfig
|
||||||
import oupson.apng.decoder.ApngDecoder.Companion.decodeApng
|
|
||||||
import oupson.apng.drawable.ApngDrawable
|
import oupson.apng.drawable.ApngDrawable
|
||||||
import oupson.apng.exceptions.BadApngException
|
import oupson.apng.exceptions.BadApngException
|
||||||
import oupson.apng.exceptions.BadCRCException
|
import oupson.apng.exceptions.BadCRCException
|
||||||
import oupson.apng.utils.Loader
|
import oupson.apng.utils.Loader
|
||||||
import oupson.apng.utils.Utils
|
import oupson.apng.utils.Utils
|
||||||
import oupson.apng.utils.Utils.Companion.isPng
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
@ -29,21 +25,7 @@ import java.util.zip.CRC32
|
||||||
* An APNG Decoder.
|
* An APNG Decoder.
|
||||||
* Call [decodeApng]
|
* Call [decodeApng]
|
||||||
*/
|
*/
|
||||||
class ApngDecoder {
|
class ApngDecoder(input: InputStream, val config: Config) {
|
||||||
interface Callback {
|
|
||||||
/**
|
|
||||||
* Function called when the file was successfully decoded.
|
|
||||||
* @param drawable Can be an [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
|
|
||||||
*/
|
|
||||||
fun onSuccess(drawable: Drawable)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function called when something gone wrong.
|
|
||||||
* @param error The problem.
|
|
||||||
*/
|
|
||||||
fun onError(error: Exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Config(
|
class Config(
|
||||||
internal var speed: Float = 1f,
|
internal var speed: Float = 1f,
|
||||||
internal var bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888,
|
internal var bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888,
|
||||||
|
@ -71,18 +53,8 @@ class ApngDecoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private var inputStream: InputStream? = input
|
||||||
private const val TAG = "ApngDecoder"
|
private var result: Result<Drawable>? = null
|
||||||
private val zeroLength = byteArrayOf(0x00, 0x00, 0x00, 0x00)
|
|
||||||
|
|
||||||
// Paint used to clear the buffer
|
|
||||||
private val clearPaint: Paint by lazy {
|
|
||||||
Paint().apply {
|
|
||||||
xfermode = PorterDuffXfermode(
|
|
||||||
PorterDuff.Mode.CLEAR
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
||||||
|
@ -91,26 +63,20 @@ class ApngDecoder {
|
||||||
* @param config Decoder configuration
|
* @param config Decoder configuration
|
||||||
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
|
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
|
||||||
*/
|
*/
|
||||||
// TODO DOCUMENT CONFIG
|
|
||||||
@Suppress(
|
|
||||||
"MemberVisibilityCanBePrivate",
|
|
||||||
"BlockingMethodInNonBlockingContext"
|
|
||||||
) // BlockingMethodInNonBlockingContext is a warning generated by java @Throws
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
suspend fun decodeApng(
|
suspend fun decodeApng(
|
||||||
context: Context,
|
context: Context
|
||||||
inStream: InputStream,
|
): Result<Drawable> =
|
||||||
config: Config = Config()
|
kotlin.runCatching {
|
||||||
): Drawable = withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val inputStream = BufferedInputStream(inStream)
|
val inputStream = BufferedInputStream(inputStream)
|
||||||
val bytes = ByteArray(8)
|
val bytes = ByteArray(8)
|
||||||
inputStream.mark(8)
|
inputStream.mark(8)
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
inputStream.read(bytes)
|
inputStream.read(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPng(bytes)) {
|
if (Utils.isPng(bytes)) {
|
||||||
var png: ByteArrayOutputStream? = null
|
var png: ByteArrayOutputStream? = null
|
||||||
var cover: ByteArrayOutputStream? = null
|
var cover: ByteArrayOutputStream? = null
|
||||||
var delay = -1f
|
var delay = -1f
|
||||||
|
@ -120,7 +86,8 @@ class ApngDecoder {
|
||||||
var tnrs: ByteArray? = null
|
var tnrs: ByteArray? = null
|
||||||
var maxWidth = 0
|
var maxWidth = 0
|
||||||
var maxHeight = 0
|
var maxHeight = 0
|
||||||
var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
|
var blendOp: Utils.Companion.BlendOp =
|
||||||
|
Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
|
||||||
var disposeOp: Utils.Companion.DisposeOp =
|
var disposeOp: Utils.Companion.DisposeOp =
|
||||||
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
|
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
|
||||||
|
|
||||||
|
@ -142,7 +109,6 @@ class ApngDecoder {
|
||||||
if (withContext(Dispatchers.IO) {
|
if (withContext(Dispatchers.IO) {
|
||||||
byteRead = inputStream.read(lengthChunk)
|
byteRead = inputStream.read(lengthChunk)
|
||||||
|
|
||||||
|
|
||||||
if (byteRead != -1) {
|
if (byteRead != -1) {
|
||||||
length = Utils.uIntFromBytesBigEndian(lengthChunk)
|
length = Utils.uIntFromBytesBigEndian(lengthChunk)
|
||||||
|
|
||||||
|
@ -183,6 +149,7 @@ class ApngDecoder {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cover?.close()
|
||||||
cover = null
|
cover = null
|
||||||
} else {
|
} else {
|
||||||
// Add IEND body length : 0
|
// Add IEND body length : 0
|
||||||
|
@ -268,9 +235,9 @@ class ApngDecoder {
|
||||||
}
|
}
|
||||||
else -> buffer = btm
|
else -> buffer = btm
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
png?.close()
|
||||||
png = ByteArrayOutputStream(4096)
|
png = ByteArrayOutputStream(4096)
|
||||||
|
|
||||||
// Parse Frame ConTroL chunk
|
// Parse Frame ConTroL chunk
|
||||||
|
@ -352,6 +319,7 @@ class ApngDecoder {
|
||||||
)
|
)
|
||||||
|
|
||||||
val pngBytes = png.toByteArray()
|
val pngBytes = png.toByteArray()
|
||||||
|
png.close()
|
||||||
val decoded = BitmapFactory.decodeByteArray(
|
val decoded = BitmapFactory.decodeByteArray(
|
||||||
pngBytes,
|
pngBytes,
|
||||||
0,
|
0,
|
||||||
|
@ -427,11 +395,14 @@ class ApngDecoder {
|
||||||
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
||||||
it.write(Utils.IEND)
|
it.write(Utils.IEND)
|
||||||
it.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
it.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
val pngBytes = it.toByteArray()
|
val pngBytes = it.toByteArray()
|
||||||
|
it.close()
|
||||||
|
|
||||||
return@withContext BitmapDrawable(
|
return@withContext BitmapDrawable(
|
||||||
context.resources,
|
context.resources,
|
||||||
BitmapFactory.decodeByteArray(
|
BitmapFactory.decodeByteArray(
|
||||||
|
@ -531,13 +502,13 @@ class ApngDecoder {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
}
|
}
|
||||||
return@withContext drawable
|
drawable
|
||||||
} else {
|
} else {
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
Log.i(TAG, "Decoding non APNG stream")
|
Log.i(TAG, "Decoding non APNG stream")
|
||||||
inputStream.reset()
|
inputStream.reset()
|
||||||
|
|
||||||
return@withContext if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val bytesRead: ByteArray
|
val bytesRead: ByteArray
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
bytesRead = inputStream.readBytes()
|
bytesRead = inputStream.readBytes()
|
||||||
|
@ -556,10 +527,29 @@ class ApngDecoder {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
}
|
}
|
||||||
drawable
|
drawable!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getDecoded(context: Context): Result<Drawable> {
|
||||||
|
if (result == null) {
|
||||||
|
result = decodeApng(context)
|
||||||
|
|
||||||
|
kotlin.runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
inputStream?.close()
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
this.result = Result.failure(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStream = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return result ?: Result.failure(NullPointerException("result is null"))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
||||||
|
@ -568,18 +558,8 @@ class ApngDecoder {
|
||||||
* @param config Decoder configuration
|
* @param config Decoder configuration
|
||||||
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
|
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
|
||||||
*/
|
*/
|
||||||
@Suppress("unused", "BlockingMethodInNonBlockingContext")
|
// TODO DOC
|
||||||
@JvmStatic
|
constructor(file: File, config: Config = Config()) : this(FileInputStream(file), config)
|
||||||
// TODO DOCUMENT
|
|
||||||
suspend fun decodeApng(
|
|
||||||
context: Context,
|
|
||||||
file: File,
|
|
||||||
config: Config = Config()
|
|
||||||
): Drawable =
|
|
||||||
decodeApng(
|
|
||||||
context,
|
|
||||||
withContext(Dispatchers.IO) { FileInputStream(file) }, config
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
||||||
|
@ -588,20 +568,12 @@ class ApngDecoder {
|
||||||
* @param config Decoder configuration
|
* @param config Decoder configuration
|
||||||
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
// TODO DOC + better
|
||||||
@JvmStatic
|
constructor(
|
||||||
suspend fun decodeApng(
|
|
||||||
context: Context,
|
context: Context,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
config: Config = Config()
|
config: Config = Config()
|
||||||
): Drawable {
|
) : this(context.contentResolver.openInputStream(uri)!!, config)
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)!!
|
|
||||||
return decodeApng(
|
|
||||||
context,
|
|
||||||
inputStream,
|
|
||||||
config
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
||||||
|
@ -610,287 +582,13 @@ class ApngDecoder {
|
||||||
* @param config Decoder configuration
|
* @param config Decoder configuration
|
||||||
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
// TODO DOC
|
||||||
@JvmStatic
|
constructor(
|
||||||
suspend fun decodeApng(
|
|
||||||
context: Context,
|
context: Context,
|
||||||
@RawRes res: Int,
|
@RawRes res: Int,
|
||||||
config: Config = Config()
|
config: Config = Config()
|
||||||
): Drawable =
|
) : this(context.resources.openRawResource(res), config)
|
||||||
decodeApng(
|
|
||||||
context,
|
|
||||||
context.resources.openRawResource(res),
|
|
||||||
config
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
|
||||||
* @param context Context is needed for contentResolver and animation drawable.
|
|
||||||
* @param url URL to decode.
|
|
||||||
* @param config Decoder configuration
|
|
||||||
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
|
||||||
*/
|
|
||||||
@Suppress("unused", "BlockingMethodInNonBlockingContext")
|
|
||||||
@JvmStatic
|
|
||||||
suspend fun decodeApng(
|
|
||||||
context: Context,
|
|
||||||
url: URL,
|
|
||||||
config: Config = Config()
|
|
||||||
) =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
decodeApng(
|
|
||||||
context,
|
|
||||||
ByteArrayInputStream(Loader.load(url)),
|
|
||||||
config
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Apng into an imageView, asynchronously.
|
|
||||||
* @param context Context needed for animation drawable.
|
|
||||||
* @param file File to decode.
|
|
||||||
* @param imageView Image View.
|
|
||||||
* @param callback [ApngDecoder.Callback] to handle success and error.
|
|
||||||
* @param config Decoder configuration
|
|
||||||
*/
|
|
||||||
@Suppress("unused", "BlockingMethodInNonBlockingContext")
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun decodeApngAsyncInto(
|
|
||||||
context: Context,
|
|
||||||
file: File,
|
|
||||||
imageView: ImageView,
|
|
||||||
callback: Callback? = null,
|
|
||||||
config: Config = Config(),
|
|
||||||
scope: CoroutineScope = GlobalScope
|
|
||||||
) {
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
try {
|
|
||||||
val drawable =
|
|
||||||
decodeApng(
|
|
||||||
context,
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
FileInputStream(file)
|
|
||||||
},
|
|
||||||
config
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
imageView.setImageDrawable(drawable)
|
|
||||||
(drawable as? AnimationDrawable)?.start()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
(drawable as? AnimatedImageDrawable)?.start()
|
|
||||||
}
|
|
||||||
callback?.onSuccess(drawable)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
callback?.onError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Apng into an imageView, asynchronously.
|
|
||||||
* @param context Context needed for animation drawable and content resolver.
|
|
||||||
* @param uri Uri to load.
|
|
||||||
* @param imageView Image View.
|
|
||||||
* @param callback [ApngDecoder.Callback] to handle success and error.
|
|
||||||
* @param config Decoder configuration
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun decodeApngAsyncInto(
|
|
||||||
context: Context,
|
|
||||||
uri: Uri,
|
|
||||||
imageView: ImageView,
|
|
||||||
callback: Callback? = null,
|
|
||||||
config: Config = Config(),
|
|
||||||
scope: CoroutineScope = GlobalScope
|
|
||||||
) {
|
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)!!
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
try {
|
|
||||||
val drawable =
|
|
||||||
decodeApng(
|
|
||||||
context,
|
|
||||||
inputStream,
|
|
||||||
config
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
imageView.setImageDrawable(drawable)
|
|
||||||
(drawable as? AnimationDrawable)?.start()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
(drawable as? AnimatedImageDrawable)?.start()
|
|
||||||
}
|
|
||||||
callback?.onSuccess(drawable)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
callback?.onError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Apng into an imageView, asynchronously.
|
|
||||||
* @param context Context needed to decode the resource and for the animation drawable.
|
|
||||||
* @param res Raw resource to load.
|
|
||||||
* @param imageView Image View.
|
|
||||||
* @param callback [ApngDecoder.Callback] to handle success and error.
|
|
||||||
* @param config Decoder configuration
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun decodeApngAsyncInto(
|
|
||||||
context: Context, @RawRes res: Int,
|
|
||||||
imageView: ImageView,
|
|
||||||
callback: Callback? = null,
|
|
||||||
config: Config = Config(),
|
|
||||||
scope: CoroutineScope = GlobalScope
|
|
||||||
) {
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
try {
|
|
||||||
val drawable =
|
|
||||||
decodeApng(
|
|
||||||
context,
|
|
||||||
context.resources.openRawResource(res),
|
|
||||||
config
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
imageView.setImageDrawable(drawable)
|
|
||||||
(drawable as? AnimationDrawable)?.start()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
(drawable as? AnimatedImageDrawable)?.start()
|
|
||||||
}
|
|
||||||
callback?.onSuccess(drawable)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
callback?.onError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Apng into an imageView, asynchronously.
|
|
||||||
* @param context Context needed for the animation drawable.
|
|
||||||
* @param url URL to load.
|
|
||||||
* @param imageView Image View.
|
|
||||||
* @param callback [ApngDecoder.Callback] to handle success and error.
|
|
||||||
* @param config Decoder configuration
|
|
||||||
*/
|
|
||||||
@Suppress("unused", "BlockingMethodInNonBlockingContext")
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun decodeApngAsyncInto(
|
|
||||||
context: Context,
|
|
||||||
url: URL,
|
|
||||||
imageView: ImageView,
|
|
||||||
callback: Callback? = null,
|
|
||||||
config: Config = Config(),
|
|
||||||
scope: CoroutineScope = GlobalScope
|
|
||||||
) {
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
try {
|
|
||||||
val drawable = decodeApng(
|
|
||||||
context,
|
|
||||||
ByteArrayInputStream(
|
|
||||||
Loader.load(
|
|
||||||
url
|
|
||||||
)
|
|
||||||
),
|
|
||||||
config
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
imageView.setImageDrawable(drawable)
|
|
||||||
(drawable as? AnimationDrawable)?.start()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
(drawable as? AnimatedImageDrawable)?.start()
|
|
||||||
}
|
|
||||||
callback?.onSuccess(drawable)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
callback?.onError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Apng into an imageView, asynchronously.
|
|
||||||
* @param context Context needed for decoding the image and creating the animation drawable.
|
|
||||||
* @param string URL to load
|
|
||||||
* @param imageView Image View.
|
|
||||||
* @param callback [ApngDecoder.Callback] to handle success and error.
|
|
||||||
* @param config Decoder configuration
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun decodeApngAsyncInto(
|
|
||||||
context: Context,
|
|
||||||
string: String,
|
|
||||||
imageView: ImageView,
|
|
||||||
callback: Callback? = null,
|
|
||||||
config: Config = Config(),
|
|
||||||
scope: CoroutineScope = GlobalScope
|
|
||||||
) {
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
try {
|
|
||||||
if (string.startsWith("http://") || string.startsWith("https://")) {
|
|
||||||
decodeApngAsyncInto(
|
|
||||||
context,
|
|
||||||
URL(string),
|
|
||||||
imageView,
|
|
||||||
callback,
|
|
||||||
config
|
|
||||||
)
|
|
||||||
} else if (File(string).exists()) {
|
|
||||||
var pathToLoad =
|
|
||||||
if (string.startsWith("content://")) string else "file://$string"
|
|
||||||
pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
|
|
||||||
decodeApngAsyncInto(
|
|
||||||
context,
|
|
||||||
Uri.parse(pathToLoad),
|
|
||||||
imageView,
|
|
||||||
callback,
|
|
||||||
config
|
|
||||||
)
|
|
||||||
} else if (string.startsWith("file://android_asset/")) {
|
|
||||||
val drawable =
|
|
||||||
decodeApng(
|
|
||||||
context,
|
|
||||||
context.assets.open(string.replace("file:///android_asset/", "")),
|
|
||||||
|
|
||||||
config
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
imageView.setImageDrawable(drawable)
|
|
||||||
(drawable as? AnimationDrawable)?.start()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
(drawable as? AnimatedImageDrawable)?.start()
|
|
||||||
}
|
|
||||||
callback?.onSuccess(drawable)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
callback?.onError(Exception("Cannot open string"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
callback?.onError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a correct IHDR from the IHDR chunk of the APNG.
|
* Generate a correct IHDR from the IHDR chunk of the APNG.
|
||||||
|
@ -928,5 +626,41 @@ class ApngDecoder {
|
||||||
System.arraycopy(Utils.uIntToByteArray(crC32.value.toInt()), 0, ihdr, 0xD + 4 + 4, 4)
|
System.arraycopy(Utils.uIntToByteArray(crC32.value.toInt()), 0, ihdr, 0xD + 4 + 4, 4)
|
||||||
return ihdr
|
return ihdr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ApngDecoder"
|
||||||
|
private val zeroLength = byteArrayOf(0x00, 0x00, 0x00, 0x00)
|
||||||
|
|
||||||
|
// Paint used to clear the buffer
|
||||||
|
private val clearPaint: Paint by lazy {
|
||||||
|
Paint().apply {
|
||||||
|
xfermode = PorterDuffXfermode(
|
||||||
|
PorterDuff.Mode.CLEAR
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
|
||||||
|
* @param context Context is needed for contentResolver and animation drawable.
|
||||||
|
* @param url URL to decode.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
@JvmStatic
|
||||||
|
suspend fun constructFromUrl(
|
||||||
|
url: URL,
|
||||||
|
config: Config = Config()
|
||||||
|
): Result<ApngDecoder> =
|
||||||
|
kotlin.runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ApngDecoder(
|
||||||
|
ByteArrayInputStream(Loader.load(url)),
|
||||||
|
config
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,347 @@
|
||||||
|
package oupson.apng.decoder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.AnimatedImageDrawable
|
||||||
|
import android.graphics.drawable.AnimationDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.annotation.RawRes
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import oupson.apng.drawable.ApngDrawable
|
||||||
|
import oupson.apng.utils.Utils.Companion.mapResult
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class ApngLoader(parent: Job? = null) {
|
||||||
|
interface Callback {
|
||||||
|
/**
|
||||||
|
* Function called when the file was successfully decoded.
|
||||||
|
* @param drawable Can be an [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
|
||||||
|
*/
|
||||||
|
fun onSuccess(drawable: Drawable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when something gone wrong.
|
||||||
|
* @param error The problem.
|
||||||
|
*/
|
||||||
|
fun onError(error: Throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val job = SupervisorJob(parent)
|
||||||
|
private val coroutineScope: CoroutineScope = CoroutineScope(job)
|
||||||
|
|
||||||
|
fun cancelAll() {
|
||||||
|
coroutineScope.cancel(CancellationException("Loading was canceled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView.
|
||||||
|
* @param context Context needed for animation drawable.
|
||||||
|
* @param file File to decode.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
suspend fun decodeApngInto(
|
||||||
|
context: Context,
|
||||||
|
file: File,
|
||||||
|
imageView: ImageView,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
): Result<Drawable> =
|
||||||
|
kotlin.runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
FileInputStream(file)
|
||||||
|
}
|
||||||
|
}.mapResult { input ->
|
||||||
|
ApngDecoder(
|
||||||
|
input,
|
||||||
|
config
|
||||||
|
).getDecoded(context)
|
||||||
|
}.onSuccess { drawable ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
(drawable as? AnimationDrawable)?.start()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
(drawable as? AnimatedImageDrawable)?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView.
|
||||||
|
* @param context Context needed for animation drawable and content resolver.
|
||||||
|
* @param uri Uri to load.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
suspend fun decodeApngInto(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
imageView: ImageView,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
): Result<Drawable> =
|
||||||
|
kotlin.runCatching {
|
||||||
|
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }!!
|
||||||
|
}.mapResult { inputStream ->
|
||||||
|
ApngDecoder(
|
||||||
|
inputStream,
|
||||||
|
config
|
||||||
|
).getDecoded(context)
|
||||||
|
}.onSuccess { drawable ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
(drawable as? AnimationDrawable)?.start()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
(drawable as? AnimatedImageDrawable)?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView.
|
||||||
|
* @param context Context needed to decode the resource and for the animation drawable.
|
||||||
|
* @param res Raw resource to load.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
suspend fun decodeApngInto(
|
||||||
|
context: Context, @RawRes res: Int,
|
||||||
|
imageView: ImageView,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
): Result<Drawable> =
|
||||||
|
ApngDecoder(
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
context.resources.openRawResource(res)
|
||||||
|
},
|
||||||
|
config
|
||||||
|
).getDecoded(context)
|
||||||
|
.onSuccess { drawable ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
(drawable as? AnimationDrawable)?.start()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
(drawable as? AnimatedImageDrawable)?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView, asynchronously.
|
||||||
|
* @param context Context needed for the animation drawable.
|
||||||
|
* @param url URL to load.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
suspend fun decodeApngInto(
|
||||||
|
context: Context,
|
||||||
|
url: URL,
|
||||||
|
imageView: ImageView,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
): Result<Drawable> =
|
||||||
|
ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) }
|
||||||
|
.getDecoded(context)
|
||||||
|
.onSuccess { drawable ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
(drawable as? AnimationDrawable)?.start()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
(drawable as? AnimatedImageDrawable)?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView, asynchronously.
|
||||||
|
* @param context Context needed for decoding the image and creating the animation drawable.
|
||||||
|
* @param string URL to load
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
suspend fun decodeApngInto(
|
||||||
|
context: Context,
|
||||||
|
string: String,
|
||||||
|
imageView: ImageView,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
): Result<Drawable> {
|
||||||
|
return if (string.startsWith("http://") || string.startsWith("https://")) {
|
||||||
|
kotlin.runCatching { URL(string) }
|
||||||
|
.mapResult { url ->
|
||||||
|
decodeApngInto(
|
||||||
|
context,
|
||||||
|
url,
|
||||||
|
imageView,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (File(string).exists()) {
|
||||||
|
var pathToLoad =
|
||||||
|
if (string.startsWith("content://")) string else "file://$string"
|
||||||
|
pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
|
||||||
|
decodeApngInto(
|
||||||
|
context,
|
||||||
|
Uri.parse(pathToLoad),
|
||||||
|
imageView,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
} else if (string.startsWith("file://android_asset/")) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
context.assets.open(string.replace("file:///android_asset/", ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mapResult { inputStream -> ApngDecoder(inputStream, config).getDecoded(context) }
|
||||||
|
.onSuccess { drawable ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
(drawable as? AnimationDrawable)?.start()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
(drawable as? AnimatedImageDrawable)?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception("Cannot open string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// region with callback
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView, asynchronously.
|
||||||
|
* @param context Context needed for animation drawable.
|
||||||
|
* @param file File to decode.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param callback [ApngLoader.Callback] to handle success and error.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
@JvmOverloads
|
||||||
|
fun decodeApngAsyncInto(
|
||||||
|
context: Context,
|
||||||
|
file: File,
|
||||||
|
imageView: ImageView,
|
||||||
|
callback: Callback? = null,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
) =
|
||||||
|
coroutineScope.launch(Dispatchers.Default) {
|
||||||
|
val drawable = decodeApngInto(context, file, imageView, config)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
drawable
|
||||||
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
|
.onFailure { callback?.onError(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView, asynchronously.
|
||||||
|
* @param context Context needed for animation drawable and content resolver.
|
||||||
|
* @param uri Uri to load.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param callback [ApngLoader.Callback] to handle success and error.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
@JvmOverloads
|
||||||
|
fun decodeApngAsyncInto(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
imageView: ImageView,
|
||||||
|
callback: Callback? = null,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
) = coroutineScope.launch(Dispatchers.Default) {
|
||||||
|
val drawable = decodeApngInto(context, uri, imageView, config)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
drawable
|
||||||
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
|
.onFailure { callback?.onError(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView, asynchronously.
|
||||||
|
* @param context Context needed to decode the resource and for the animation drawable.
|
||||||
|
* @param res Raw resource to load.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param callback [ApngLoader.Callback] to handle success and error.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
@JvmOverloads
|
||||||
|
fun decodeApngAsyncInto(
|
||||||
|
context: Context, @RawRes res: Int,
|
||||||
|
imageView: ImageView,
|
||||||
|
callback: Callback? = null,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
) = coroutineScope.launch(Dispatchers.Default) {
|
||||||
|
val drawable = decodeApngInto(context, res, imageView, config)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
drawable
|
||||||
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
|
.onFailure { callback?.onError(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView, asynchronously.
|
||||||
|
* @param context Context needed for the animation drawable.
|
||||||
|
* @param url URL to load.
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param callback [ApngLoader.Callback] to handle success and error.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
@JvmOverloads
|
||||||
|
fun decodeApngAsyncInto(
|
||||||
|
context: Context,
|
||||||
|
url: URL,
|
||||||
|
imageView: ImageView,
|
||||||
|
callback: Callback? = null,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
) = coroutineScope.launch(Dispatchers.Default) {
|
||||||
|
val drawable = decodeApngInto(context, url, imageView, config)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
drawable
|
||||||
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
|
.onFailure { callback?.onError(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Apng into an imageView, asynchronously.
|
||||||
|
* @param context Context needed for decoding the image and creating the animation drawable.
|
||||||
|
* @param string URL to load
|
||||||
|
* @param imageView Image View.
|
||||||
|
* @param callback [ApngLoader.Callback] to handle success and error.
|
||||||
|
* @param config Decoder configuration
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
@JvmOverloads
|
||||||
|
fun decodeApngAsyncInto(
|
||||||
|
context: Context,
|
||||||
|
string: String,
|
||||||
|
imageView: ImageView,
|
||||||
|
callback: Callback? = null,
|
||||||
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
|
) =
|
||||||
|
coroutineScope.launch(Dispatchers.Default) {
|
||||||
|
val drawable = decodeApngInto(context, string, imageView, config)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (drawable.isSuccess) {
|
||||||
|
callback?.onSuccess(drawable.getOrNull()!!)
|
||||||
|
} else {
|
||||||
|
callback?.onError(drawable.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion with callback
|
||||||
|
}
|
|
@ -291,7 +291,7 @@ class ApngEncoder(
|
||||||
) {
|
) {
|
||||||
if (currentFrame == 0) {
|
if (currentFrame == 0) {
|
||||||
if (btm.width != width || btm.height != height)
|
if (btm.width != width || btm.height != height)
|
||||||
throw InvalidFrameSizeException(
|
throw InvalidFrameSizeException( // TODO
|
||||||
btm.width,
|
btm.width,
|
||||||
btm.height,
|
btm.height,
|
||||||
width,
|
width,
|
||||||
|
|
|
@ -24,8 +24,11 @@ class Loader {
|
||||||
val connection = url.openConnection() as HttpURLConnection
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
connection.useCaches = true
|
connection.useCaches = true
|
||||||
connection.connect()
|
connection.connect()
|
||||||
|
|
||||||
|
val inputStream = connection.inputStream
|
||||||
|
|
||||||
if (connection.responseCode == 200) {
|
if (connection.responseCode == 200) {
|
||||||
val input = BufferedInputStream(connection.inputStream)
|
val input = BufferedInputStream(inputStream)
|
||||||
val output = ByteArrayOutputStream()
|
val output = ByteArrayOutputStream()
|
||||||
var bytesRead: Int
|
var bytesRead: Int
|
||||||
val buffer = ByteArray(4096)
|
val buffer = ByteArray(4096)
|
||||||
|
@ -36,9 +39,13 @@ class Loader {
|
||||||
} while (bytesRead != -1)
|
} while (bytesRead != -1)
|
||||||
input.close()
|
input.close()
|
||||||
output.close()
|
output.close()
|
||||||
|
|
||||||
|
inputStream.close()
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
|
|
||||||
output.toByteArray()
|
output.toByteArray()
|
||||||
} else {
|
} else {
|
||||||
|
inputStream.close()
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
throw Exception("Error when downloading file : ${connection.responseCode}")
|
throw Exception("Error when downloading file : ${connection.responseCode}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,7 +208,7 @@ class Utils {
|
||||||
(bytes[1] and 0xFF))
|
(bytes[1] and 0xFF))
|
||||||
|
|
||||||
// TODO DOCUMENT AND TEST
|
// TODO DOCUMENT AND TEST
|
||||||
fun uShortFromBytesBigEndian(bytes: ByteArray, offset : Int = 0): Int =
|
fun uShortFromBytesBigEndian(bytes: ByteArray, offset: Int = 0): Int =
|
||||||
(((bytes[offset].toInt() and 0xFF) shl 8) or
|
(((bytes[offset].toInt() and 0xFF) shl 8) or
|
||||||
(bytes[offset + 1].toInt() and 0xFF))
|
(bytes[offset + 1].toInt() and 0xFF))
|
||||||
|
|
||||||
|
@ -330,5 +330,14 @@ class Utils {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T, U> Result<T>.mapResult(block: suspend (T) -> Result<U>): Result<U> {
|
||||||
|
return this.fold(
|
||||||
|
onSuccess = {
|
||||||
|
block.invoke(it)
|
||||||
|
},
|
||||||
|
onFailure = { Result.failure(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
|
@ -24,29 +23,35 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
|
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
|
|
||||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
implementation("io.coil-kt:coil:1.2.2")
|
||||||
|
|
||||||
implementation project(":apng_library")
|
implementation project(":apng_library")
|
||||||
// implementation fileTree(include: ['*.aar'], dir: 'libs')
|
// implementation fileTree(include: ['*.aar'], dir: 'libs')
|
||||||
|
|
|
@ -14,18 +14,18 @@ import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.activity_creator.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import oupson.apng.encoder.ApngEncoder
|
import oupson.apng.encoder.ApngEncoder
|
||||||
|
@ -33,6 +33,7 @@ import oupson.apng.utils.Utils
|
||||||
import oupson.apngcreator.BuildConfig
|
import oupson.apngcreator.BuildConfig
|
||||||
import oupson.apngcreator.R
|
import oupson.apngcreator.R
|
||||||
import oupson.apngcreator.adapter.ImageAdapter
|
import oupson.apngcreator.adapter.ImageAdapter
|
||||||
|
import oupson.apngcreator.databinding.ActivityCreatorBinding
|
||||||
import oupson.apngcreator.dialogs.DelayInputDialog
|
import oupson.apngcreator.dialogs.DelayInputDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
@ -42,8 +43,6 @@ import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class CreatorActivity : AppCompatActivity() {
|
class CreatorActivity : AppCompatActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val PICK_IMAGE = 1
|
|
||||||
private const val WRITE_REQUEST_CODE = 2
|
|
||||||
private const val TAG = "CreatorActivity"
|
private const val TAG = "CreatorActivity"
|
||||||
|
|
||||||
private const val CREATION_CHANNEL_ID =
|
private const val CREATION_CHANNEL_ID =
|
||||||
|
@ -57,35 +56,97 @@ class CreatorActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var nextImageId: Long = 0
|
private var nextImageId: Long = 0
|
||||||
|
|
||||||
|
private var binding: ActivityCreatorBinding? = null
|
||||||
|
|
||||||
|
private val pickLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val data: Intent? = result.data
|
||||||
|
|
||||||
|
if (data?.clipData != null) {
|
||||||
|
for (i in 0 until data.clipData!!.itemCount) {
|
||||||
|
items.add(Triple(data.clipData!!.getItemAt(i).uri, 1000, nextImageId++))
|
||||||
|
}
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
} else if (data?.data != null) {
|
||||||
|
items.add(Triple(data.data!!, 1000, nextImageId++))
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val writeLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val data: Intent? = result.data
|
||||||
|
|
||||||
|
if (data?.data != null) {
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
Log.i(TAG, "Intent data : ${data.data}")
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(this, CREATION_CHANNEL_ID).apply {
|
||||||
|
setContentTitle(getString(R.string.create_notification_title))
|
||||||
|
setContentText(
|
||||||
|
this@CreatorActivity.resources.getQuantityString(
|
||||||
|
R.plurals.create_notification_description,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
items.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setSmallIcon(R.drawable.ic_create_white_24dp)
|
||||||
|
priority = NotificationCompat.PRIORITY_LOW
|
||||||
|
}
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val out = contentResolver.openOutputStream(data.data!!) ?: return@launch
|
||||||
|
saveToOutputStream(
|
||||||
|
items.map { Pair(it.first, it.second) },
|
||||||
|
out,
|
||||||
|
builder = builder
|
||||||
|
)
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
if (binding != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Snackbar.make(
|
||||||
|
binding!!.imageRecyclerView,
|
||||||
|
R.string.done,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityCreatorBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
setContentView(R.layout.activity_creator)
|
setContentView(binding?.root)
|
||||||
|
|
||||||
fabAddImage.setOnClickListener {
|
binding?.fabAddImage?.setOnClickListener {
|
||||||
val getIntent = Intent(Intent.ACTION_GET_CONTENT)
|
val getIntent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
getIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
getIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
getIntent.type = "image/*"
|
getIntent.type = "image/*"
|
||||||
|
|
||||||
startActivityForResult(
|
pickLauncher.launch(getIntent)
|
||||||
getIntent,
|
|
||||||
PICK_IMAGE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter = ImageAdapter(this, items)
|
adapter = ImageAdapter(this, items, lifecycleScope)
|
||||||
adapter?.setHasStableIds(true)
|
adapter?.setHasStableIds(true)
|
||||||
|
|
||||||
imageRecyclerView.layoutManager = LinearLayoutManager(this)
|
binding?.imageRecyclerView?.layoutManager = LinearLayoutManager(this)
|
||||||
imageRecyclerView.setHasFixedSize(true)
|
binding?.imageRecyclerView?.setHasFixedSize(true)
|
||||||
imageRecyclerView.itemAnimator = object : DefaultItemAnimator() {
|
binding?.imageRecyclerView?.itemAnimator = object : DefaultItemAnimator() {
|
||||||
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
|
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
imageRecyclerView.setItemViewCacheSize(20)
|
binding?.imageRecyclerView?.setItemViewCacheSize(20)
|
||||||
if (adapter != null)
|
if (adapter != null)
|
||||||
ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(imageRecyclerView)
|
ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(binding?.imageRecyclerView)
|
||||||
|
|
||||||
adapter?.clickListener = { position ->
|
adapter?.clickListener = { position ->
|
||||||
DelayInputDialog(object : DelayInputDialog.InputSenderDialogListener {
|
DelayInputDialog(object : DelayInputDialog.InputSenderDialogListener {
|
||||||
|
@ -104,8 +165,8 @@ class CreatorActivity : AppCompatActivity() {
|
||||||
}, items[position].second).show(supportFragmentManager, null)
|
}, items[position].second).show(supportFragmentManager, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
setSupportActionBar(creatorBottomAppBar)
|
setSupportActionBar(binding?.creatorBottomAppBar)
|
||||||
imageRecyclerView.adapter = adapter
|
binding?.imageRecyclerView?.adapter = adapter
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
@ -148,7 +209,7 @@ class CreatorActivity : AppCompatActivity() {
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
priority = NotificationCompat.PRIORITY_LOW
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val randomFileName = UUID.randomUUID().toString()
|
val randomFileName = UUID.randomUUID().toString()
|
||||||
val f = File(filesDir, "images/$randomFileName.png").apply {
|
val f = File(filesDir, "images/$randomFileName.png").apply {
|
||||||
if (!exists()) {
|
if (!exists()) {
|
||||||
|
@ -205,7 +266,7 @@ class CreatorActivity : AppCompatActivity() {
|
||||||
setSmallIcon(R.drawable.ic_create_white_24dp)
|
setSmallIcon(R.drawable.ic_create_white_24dp)
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
priority = NotificationCompat.PRIORITY_LOW
|
||||||
}
|
}
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val randomFileName = UUID.randomUUID().toString()
|
val randomFileName = UUID.randomUUID().toString()
|
||||||
val f = File(filesDir, "images/$randomFileName.png").apply {
|
val f = File(filesDir, "images/$randomFileName.png").apply {
|
||||||
if (!exists()) {
|
if (!exists()) {
|
||||||
|
@ -272,7 +333,10 @@ class CreatorActivity : AppCompatActivity() {
|
||||||
type = "image/png"
|
type = "image/png"
|
||||||
putExtra(Intent.EXTRA_TITLE, "${items[0].first.lastPathSegment}.png")
|
putExtra(Intent.EXTRA_TITLE, "${items[0].first.lastPathSegment}.png")
|
||||||
}
|
}
|
||||||
startActivityForResult(intent, WRITE_REQUEST_CODE)
|
|
||||||
|
writeLauncher.launch(
|
||||||
|
intent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -409,7 +473,9 @@ class CreatorActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
encoder.writeEnd()
|
encoder.writeEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (builder != null) {
|
if (builder != null) {
|
||||||
|
@ -423,72 +489,15 @@ class CreatorActivity : AppCompatActivity() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
when (requestCode) {
|
|
||||||
PICK_IMAGE -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
if (data?.clipData != null) {
|
|
||||||
for (i in 0 until data.clipData!!.itemCount) {
|
|
||||||
items.add(Triple(data.clipData!!.getItemAt(i).uri, 1000, nextImageId++))
|
|
||||||
}
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
} else if (data?.data != null) {
|
|
||||||
items.add(Triple(data.data!!, 1000, nextImageId++))
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WRITE_REQUEST_CODE -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
if (data?.data != null) {
|
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
Log.i(TAG, "Intent data : ${data.data}")
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(this, CREATION_CHANNEL_ID).apply {
|
|
||||||
setContentTitle(getString(R.string.create_notification_title))
|
|
||||||
setContentText(
|
|
||||||
this@CreatorActivity.resources.getQuantityString(
|
|
||||||
R.plurals.create_notification_description,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
items.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setSmallIcon(R.drawable.ic_create_white_24dp)
|
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
|
||||||
}
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
|
|
||||||
val out = contentResolver.openOutputStream(data.data!!) ?: return@launch
|
|
||||||
saveToOutputStream(
|
|
||||||
items.map { Pair(it.first, it.second) },
|
|
||||||
out,
|
|
||||||
builder = builder
|
|
||||||
)
|
|
||||||
out.close()
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Snackbar.make(
|
|
||||||
imageRecyclerView,
|
|
||||||
R.string.done,
|
|
||||||
Snackbar.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val deleteResult = File(filesDir, "images").deleteRecursively()
|
val deleteResult = File(filesDir, "images").deleteRecursively()
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
Log.v(TAG, "Deleted images dir : $deleteResult")
|
Log.v(TAG, "Deleted images dir : $deleteResult")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inner class SwipeToDeleteCallback(private val adapter: ImageAdapter) :
|
inner class SwipeToDeleteCallback(private val adapter: ImageAdapter) :
|
||||||
ItemTouchHelper.SimpleCallback(
|
ItemTouchHelper.SimpleCallback(
|
||||||
|
|
|
@ -4,19 +4,25 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.http.HttpResponseCache
|
import android.net.http.HttpResponseCache
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.StrictMode
|
||||||
|
import android.os.StrictMode.ThreadPolicy
|
||||||
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.bottomappbar.BottomAppBarTopEdgeTreatment
|
import com.google.android.material.bottomappbar.BottomAppBarTopEdgeTreatment
|
||||||
import com.google.android.material.shape.CutCornerTreatment
|
import com.google.android.material.shape.CutCornerTreatment
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
import com.google.android.material.shape.ShapePath
|
import com.google.android.material.shape.ShapePath
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import oupson.apngcreator.BuildConfig
|
import oupson.apngcreator.BuildConfig
|
||||||
import oupson.apngcreator.R
|
import oupson.apngcreator.R
|
||||||
|
import oupson.apngcreator.databinding.ActivityMainBinding
|
||||||
import oupson.apngcreator.fragments.ApngDecoderFragment
|
import oupson.apngcreator.fragments.ApngDecoderFragment
|
||||||
import oupson.apngcreator.fragments.JavaFragment
|
import oupson.apngcreator.fragments.JavaFragment
|
||||||
import oupson.apngcreator.fragments.KotlinFragment
|
import oupson.apngcreator.fragments.KotlinFragment
|
||||||
|
@ -28,38 +34,67 @@ class MainActivity : AppCompatActivity() {
|
||||||
private const val TAG = "MainActivity"
|
private const val TAG = "MainActivity"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var binding: ActivityMainBinding? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
StrictMode.setThreadPolicy(
|
||||||
|
ThreadPolicy.Builder()
|
||||||
|
.detectDiskReads()
|
||||||
|
.detectDiskWrites()
|
||||||
|
.detectAll()
|
||||||
|
.penaltyLog()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
StrictMode.setVmPolicy(
|
||||||
|
VmPolicy.Builder()
|
||||||
|
.detectLeakedSqlLiteObjects()
|
||||||
|
.detectLeakedClosableObjects()
|
||||||
|
.penaltyLog()
|
||||||
|
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
Log.v(TAG, "supportFragmentManager.fragments.size : ${supportFragmentManager.fragments.size}")
|
Log.v(
|
||||||
|
TAG,
|
||||||
|
"supportFragmentManager.fragments.size : ${supportFragmentManager.fragments.size}"
|
||||||
|
)
|
||||||
|
|
||||||
setContentView(R.layout.activity_main)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
setSupportActionBar(bottomAppBar)
|
|
||||||
|
setContentView(binding?.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding?.bottomAppBar)
|
||||||
|
|
||||||
setUpBottomAppBarShapeAppearance()
|
setUpBottomAppBarShapeAppearance()
|
||||||
|
|
||||||
val httpCacheSize = 10 * 1024 * 1024.toLong() // 10 MiB
|
val httpCacheSize = 10 * 1024 * 1024.toLong() // 10 MiB
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val httpCacheDir = File(cacheDir, "http")
|
val httpCacheDir = File(cacheDir, "http")
|
||||||
HttpResponseCache.install(httpCacheDir, httpCacheSize)
|
HttpResponseCache.install(httpCacheDir, httpCacheSize)
|
||||||
|
}
|
||||||
|
|
||||||
fabCreate.setOnClickListener {
|
binding?.fabCreate?.setOnClickListener {
|
||||||
startActivity(Intent(this, CreatorActivity::class.java))
|
startActivity(Intent(this, CreatorActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
val drawerToggle = ActionBarDrawerToggle(this, drawer_layout, bottomAppBar,
|
val drawerToggle = ActionBarDrawerToggle(
|
||||||
|
this, binding?.drawerLayout, binding?.bottomAppBar,
|
||||||
R.string.open,
|
R.string.open,
|
||||||
R.string.close
|
R.string.close
|
||||||
)
|
)
|
||||||
drawer_layout.addDrawerListener(drawerToggle)
|
binding?.drawerLayout?.addDrawerListener(drawerToggle)
|
||||||
drawerToggle.syncState()
|
drawerToggle.syncState()
|
||||||
|
|
||||||
var selected = 0
|
var selected = 0
|
||||||
|
|
||||||
navigationView.setNavigationItemSelectedListener { menuItem : MenuItem ->
|
binding?.navigationView?.setNavigationItemSelectedListener { menuItem: MenuItem ->
|
||||||
when(menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
R.id.menu_kotlin_fragment -> {
|
R.id.menu_kotlin_fragment -> {
|
||||||
if (selected != 0) {
|
if (selected != 0) {
|
||||||
supportFragmentManager.beginTransaction().apply {
|
supportFragmentManager.beginTransaction().apply {
|
||||||
|
@ -98,20 +133,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawer_layout.closeDrawer(GravityCompat.START)
|
binding?.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||||
|
|
||||||
return@setNavigationItemSelectedListener true
|
return@setNavigationItemSelectedListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent.hasExtra("fragment") && supportFragmentManager.fragments.size == 0) {
|
if (intent.hasExtra("fragment") && supportFragmentManager.fragments.size == 0) {
|
||||||
when(intent.getStringExtra("fragment")) {
|
when (intent.getStringExtra("fragment")) {
|
||||||
"kotlin" -> {
|
"kotlin" -> {
|
||||||
supportFragmentManager.beginTransaction().apply {
|
supportFragmentManager.beginTransaction().apply {
|
||||||
add(
|
add(
|
||||||
R.id.fragment_container,
|
R.id.fragment_container,
|
||||||
KotlinFragment.newInstance(), "KotlinFragment")
|
KotlinFragment.newInstance(), "KotlinFragment"
|
||||||
|
)
|
||||||
}.commit()
|
}.commit()
|
||||||
navigationView.setCheckedItem(R.id.menu_kotlin_fragment)
|
binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment)
|
||||||
selected = 0
|
selected = 0
|
||||||
}
|
}
|
||||||
"java" -> {
|
"java" -> {
|
||||||
|
@ -121,7 +157,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
JavaFragment()
|
JavaFragment()
|
||||||
)
|
)
|
||||||
}.commit()
|
}.commit()
|
||||||
navigationView.setCheckedItem(R.id.menu_java_fragment)
|
binding?.navigationView?.setCheckedItem(R.id.menu_java_fragment)
|
||||||
selected = 1
|
selected = 1
|
||||||
}
|
}
|
||||||
"apng_decoder" -> {
|
"apng_decoder" -> {
|
||||||
|
@ -131,7 +167,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
ApngDecoderFragment.newInstance()
|
ApngDecoderFragment.newInstance()
|
||||||
)
|
)
|
||||||
}.commit()
|
}.commit()
|
||||||
navigationView.setCheckedItem(R.id.menu_apng_decoder_fragment)
|
binding?.navigationView?.setCheckedItem(R.id.menu_apng_decoder_fragment)
|
||||||
selected = 2
|
selected = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,36 +175,42 @@ class MainActivity : AppCompatActivity() {
|
||||||
supportFragmentManager.beginTransaction().apply {
|
supportFragmentManager.beginTransaction().apply {
|
||||||
add(
|
add(
|
||||||
R.id.fragment_container,
|
R.id.fragment_container,
|
||||||
KotlinFragment.newInstance(), "KotlinFragment")
|
KotlinFragment.newInstance(), "KotlinFragment"
|
||||||
|
)
|
||||||
}.commit()
|
}.commit()
|
||||||
navigationView.setCheckedItem(R.id.menu_kotlin_fragment)
|
binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
HttpResponseCache.getInstalled()?.flush()
|
HttpResponseCache.getInstalled()?.flush()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setUpBottomAppBarShapeAppearance() {
|
private fun setUpBottomAppBarShapeAppearance() {
|
||||||
val fabShapeAppearanceModel: ShapeAppearanceModel = fabCreate.shapeAppearanceModel
|
if (binding != null) {
|
||||||
|
val fabShapeAppearanceModel: ShapeAppearanceModel =
|
||||||
|
binding!!.fabCreate.shapeAppearanceModel
|
||||||
val cutCornersFab =
|
val cutCornersFab =
|
||||||
(fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment
|
(fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment
|
||||||
&& fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment)
|
&& fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment)
|
||||||
val topEdge =
|
val topEdge =
|
||||||
if (cutCornersFab) BottomAppBarCutCornersTopEdge(
|
if (cutCornersFab) BottomAppBarCutCornersTopEdge(
|
||||||
bottomAppBar.fabCradleMargin,
|
binding!!.bottomAppBar.fabCradleMargin,
|
||||||
bottomAppBar.fabCradleRoundedCornerRadius,
|
binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
|
||||||
bottomAppBar.cradleVerticalOffset
|
binding!!.bottomAppBar.cradleVerticalOffset
|
||||||
) else BottomAppBarTopEdgeTreatment(
|
) else BottomAppBarTopEdgeTreatment(
|
||||||
bottomAppBar.fabCradleMargin,
|
binding!!.bottomAppBar.fabCradleMargin,
|
||||||
bottomAppBar.fabCradleRoundedCornerRadius,
|
binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
|
||||||
bottomAppBar.cradleVerticalOffset
|
binding!!.bottomAppBar.cradleVerticalOffset
|
||||||
)
|
)
|
||||||
val babBackground = bottomAppBar.background as MaterialShapeDrawable
|
val babBackground = binding!!.bottomAppBar.background as MaterialShapeDrawable
|
||||||
babBackground.shapeAppearanceModel =
|
babBackground.shapeAppearanceModel =
|
||||||
babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build()
|
babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
inner class BottomAppBarCutCornersTopEdge(
|
inner class BottomAppBarCutCornersTopEdge(
|
||||||
|
|
|
@ -9,14 +9,21 @@ import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.android.synthetic.main.activity_viewer.*
|
|
||||||
import oupson.apng.decoder.ApngDecoder
|
import oupson.apng.decoder.ApngDecoder
|
||||||
import oupson.apngcreator.R
|
import oupson.apng.decoder.ApngLoader
|
||||||
|
import oupson.apngcreator.databinding.ActivityViewerBinding
|
||||||
|
|
||||||
class ViewerActivity : AppCompatActivity() {
|
class ViewerActivity : AppCompatActivity() {
|
||||||
|
private var apngLoader: ApngLoader? = null
|
||||||
|
private var binding: ActivityViewerBinding? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_viewer)
|
binding = ActivityViewerBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
setContentView(binding?.root)
|
||||||
|
|
||||||
|
this.apngLoader = ApngLoader()
|
||||||
|
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
@ -50,16 +57,26 @@ class ViewerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
val uri = intent.data ?: return
|
val uri = intent.data ?: return
|
||||||
ApngDecoder.decodeApngAsyncInto(this, uri, viewerImageView, callback = object : ApngDecoder.Callback {
|
|
||||||
|
if (binding != null)
|
||||||
|
apngLoader?.decodeApngAsyncInto(
|
||||||
|
this,
|
||||||
|
uri,
|
||||||
|
binding!!.viewerImageView,
|
||||||
|
callback = object : ApngLoader.Callback {
|
||||||
override fun onSuccess(drawable: Drawable) {}
|
override fun onSuccess(drawable: Drawable) {}
|
||||||
override fun onError(error: Exception) {
|
override fun onError(error: Throwable) {
|
||||||
Log.e("ViewerActivity", "Error when loading file", error)
|
Log.e("ViewerActivity", "Error when loading file", error)
|
||||||
}
|
}
|
||||||
}, ApngDecoder.Config(decodeCoverFrame = false))
|
},
|
||||||
|
ApngDecoder.Config(decodeCoverFrame = false)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int,
|
override fun onRequestPermissionsResult(
|
||||||
permissions: Array<String>, grantResults: IntArray) {
|
requestCode: Int,
|
||||||
|
permissions: Array<String>, grantResults: IntArray
|
||||||
|
) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
2 -> {
|
2 -> {
|
||||||
|
|
|
@ -10,21 +10,25 @@ import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import oupson.apngcreator.R
|
import oupson.apngcreator.R
|
||||||
|
|
||||||
class ImageAdapter(private val context : Context, private val list : List<Triple<Uri, Int, Long>>) : RecyclerView.Adapter<ImageAdapter.ImageHolder>() {
|
class ImageAdapter(
|
||||||
inner class ImageHolder(view : View) : RecyclerView.ViewHolder(view) {
|
private val context: Context,
|
||||||
val imageView : ImageView? = view.findViewById(R.id.listImageView)
|
private val list: List<Triple<Uri, Int, Long>>,
|
||||||
val textDelay : TextView? = view.findViewById(R.id.textDelay)
|
private val scope: CoroutineScope
|
||||||
val positionTextView : TextView? = view.findViewById(R.id.position_textView)
|
) : RecyclerView.Adapter<ImageAdapter.ImageHolder>() {
|
||||||
val nameTextView : TextView? = view.findViewById(R.id.name_textView)
|
inner class ImageHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val imageView: ImageView? = view.findViewById(R.id.listImageView)
|
||||||
|
val textDelay: TextView? = view.findViewById(R.id.textDelay)
|
||||||
|
val positionTextView: TextView? = view.findViewById(R.id.position_textView)
|
||||||
|
val nameTextView: TextView? = view.findViewById(R.id.name_textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
var clickListener : ((position : Int) -> Unit)? = null
|
var clickListener: ((position: Int) -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
@ -36,7 +40,7 @@ class ImageAdapter(private val context : Context, private val list : List<Triple
|
||||||
holder.textDelay?.text = String.format("%dms", list[position].second)
|
holder.textDelay?.text = String.format("%dms", list[position].second)
|
||||||
holder.positionTextView?.text = String.format("# %03d", holder.adapterPosition + 1)
|
holder.positionTextView?.text = String.format("# %03d", holder.adapterPosition + 1)
|
||||||
holder.nameTextView?.text = list[position].first.path?.substringAfterLast("/")
|
holder.nameTextView?.text = list[position].first.path?.substringAfterLast("/")
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val inputStream = context.contentResolver.openInputStream(list[position].first)
|
val inputStream = context.contentResolver.openInputStream(list[position].first)
|
||||||
val btm =
|
val btm =
|
||||||
BitmapFactory.decodeStream(inputStream, null, BitmapFactory.Options().apply {
|
BitmapFactory.decodeStream(inputStream, null, BitmapFactory.Options().apply {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import oupson.apng.decoder.ApngDecoder
|
import oupson.apng.decoder.ApngDecoder
|
||||||
|
import oupson.apng.decoder.ApngLoader
|
||||||
import oupson.apng.drawable.ApngDrawable
|
import oupson.apng.drawable.ApngDrawable
|
||||||
import oupson.apngcreator.BuildConfig
|
import oupson.apngcreator.BuildConfig
|
||||||
import oupson.apngcreator.R
|
import oupson.apngcreator.R
|
||||||
|
@ -19,32 +20,43 @@ import java.net.URL
|
||||||
class ApngDecoderFragment : Fragment() {
|
class ApngDecoderFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ApngDecoderFragment"
|
private const val TAG = "ApngDecoderFragment"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newInstance() =
|
fun newInstance() =
|
||||||
ApngDecoderFragment()
|
ApngDecoderFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var apngLoader: ApngLoader? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_apng_decoder, container, false)
|
val view = inflater.inflate(R.layout.fragment_apng_decoder, container, false)
|
||||||
|
|
||||||
val imageView : ImageView = view.findViewById(R.id.apngDecoderImageView) ?: return view
|
val imageView: ImageView = view.findViewById(R.id.apngDecoderImageView) ?: return view
|
||||||
|
|
||||||
|
apngLoader = ApngLoader()
|
||||||
|
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
ApngDecoder.decodeApngAsyncInto(
|
apngLoader?.decodeApngAsyncInto(
|
||||||
this.requireContext(),
|
this.requireContext(),
|
||||||
URL("https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"),
|
URL("https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"),
|
||||||
imageView,
|
imageView,
|
||||||
config = ApngDecoder.Config(bitmapConfig = Bitmap.Config.RGB_565, decodeCoverFrame = true),
|
config = ApngDecoder.Config(
|
||||||
callback = object : ApngDecoder.Callback {
|
bitmapConfig = Bitmap.Config.RGB_565,
|
||||||
|
decodeCoverFrame = true
|
||||||
|
),
|
||||||
|
callback = object : ApngLoader.Callback {
|
||||||
override fun onSuccess(drawable: Drawable) {
|
override fun onSuccess(drawable: Drawable) {
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
Log.i(TAG, "onSuccess(), has cover frame : ${(drawable as? ApngDrawable)?.coverFrame != null}")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"onSuccess(), has cover frame : ${(drawable as? ApngDrawable)?.coverFrame != null}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(error: Exception) {
|
override fun onError(error: Throwable) {
|
||||||
Log.e(TAG, "onError : $error")
|
Log.e(TAG, "onError : $error")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import oupson.apng.decoder.ApngDecoder;
|
import oupson.apng.decoder.ApngDecoder;
|
||||||
|
import oupson.apng.decoder.ApngLoader;
|
||||||
import oupson.apngcreator.BuildConfig;
|
import oupson.apngcreator.BuildConfig;
|
||||||
import oupson.apngcreator.R;
|
import oupson.apngcreator.R;
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@ import oupson.apngcreator.R;
|
||||||
public class JavaFragment extends Fragment {
|
public class JavaFragment extends Fragment {
|
||||||
private static final String TAG = "JavaActivity";
|
private static final String TAG = "JavaActivity";
|
||||||
|
|
||||||
|
private ApngLoader apngLoader = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
|
@ -36,10 +39,12 @@ public class JavaFragment extends Fragment {
|
||||||
|
|
||||||
Context context = this.getContext();
|
Context context = this.getContext();
|
||||||
|
|
||||||
|
this.apngLoader = new ApngLoader();
|
||||||
|
|
||||||
if (imageView != null && context != null) {
|
if (imageView != null && context != null) {
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
Log.v(TAG, "Loading " + imageUrl);
|
Log.v(TAG, "Loading " + imageUrl);
|
||||||
ApngDecoder.decodeApngAsyncInto(context, imageUrl, imageView, new ApngDecoder.Callback() {
|
this.apngLoader.decodeApngAsyncInto(context, imageUrl, imageView, new ApngLoader.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(@NotNull Drawable drawable) {
|
public void onSuccess(@NotNull Drawable drawable) {
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
|
@ -47,7 +52,7 @@ public class JavaFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(@NotNull Exception error) {
|
public void onError(@NotNull Throwable error) {
|
||||||
Log.e(TAG, "Error : " + error.toString());
|
Log.e(TAG, "Error : " + error.toString());
|
||||||
}
|
}
|
||||||
}, new ApngDecoder.Config().setIsDecodingCoverFrame(false));
|
}, new ApngDecoder.Config().setIsDecodingCoverFrame(false));
|
||||||
|
@ -56,4 +61,11 @@ public class JavaFragment extends Fragment {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
apngLoader.cancelAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,8 @@ import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.squareup.picasso.Picasso
|
import coil.load
|
||||||
import kotlinx.android.synthetic.main.activity_creator.*
|
import oupson.apng.decoder.ApngLoader
|
||||||
import oupson.apng.decoder.ApngDecoder
|
|
||||||
import oupson.apng.drawable.ApngDrawable
|
import oupson.apng.drawable.ApngDrawable
|
||||||
import oupson.apngcreator.BuildConfig
|
import oupson.apngcreator.BuildConfig
|
||||||
import oupson.apngcreator.R
|
import oupson.apngcreator.R
|
||||||
|
@ -22,22 +21,23 @@ import oupson.apngcreator.R
|
||||||
class KotlinFragment : Fragment() {
|
class KotlinFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "KotlinFragment"
|
private const val TAG = "KotlinFragment"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newInstance() =
|
fun newInstance() =
|
||||||
KotlinFragment()
|
KotlinFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var apngImageView : ImageView? = null
|
private var apngImageView: ImageView? = null
|
||||||
private var normalImageView : ImageView? = null
|
private var normalImageView: ImageView? = null
|
||||||
|
|
||||||
private var pauseButton : Button? = null
|
private var pauseButton: Button? = null
|
||||||
private var playButton : Button? = null
|
private var playButton: Button? = null
|
||||||
|
|
||||||
private var speedSeekBar : SeekBar? = null
|
private var speedSeekBar: SeekBar? = null
|
||||||
|
|
||||||
//private var animator : ApngAnimator? = null
|
//private var animator : ApngAnimator? = null
|
||||||
private var animation : ApngDrawable? = null
|
private var animation: ApngDrawable? = null
|
||||||
private var durations : IntArray? = null
|
private var durations: IntArray? = null
|
||||||
|
|
||||||
private var frameIndex = 0
|
private var frameIndex = 0
|
||||||
|
|
||||||
|
@ -51,6 +51,8 @@ class KotlinFragment : Fragment() {
|
||||||
)
|
)
|
||||||
private val selected = 4
|
private val selected = 4
|
||||||
|
|
||||||
|
private var apngLoader: ApngLoader? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
|
@ -60,6 +62,8 @@ class KotlinFragment : Fragment() {
|
||||||
|
|
||||||
val view = inflater.inflate(R.layout.fragment_kotlin, container, false)
|
val view = inflater.inflate(R.layout.fragment_kotlin, container, false)
|
||||||
|
|
||||||
|
apngLoader = ApngLoader()
|
||||||
|
|
||||||
apngImageView = view.findViewById(R.id.ApngImageView)
|
apngImageView = view.findViewById(R.id.ApngImageView)
|
||||||
normalImageView = view.findViewById(R.id.NormalImageView)
|
normalImageView = view.findViewById(R.id.NormalImageView)
|
||||||
|
|
||||||
|
@ -126,7 +130,10 @@ class KotlinFragment : Fragment() {
|
||||||
res.coverFrame = animation.coverFrame
|
res.coverFrame = animation.coverFrame
|
||||||
|
|
||||||
for (i in 0 until animation.numberOfFrames) {
|
for (i in 0 until animation.numberOfFrames) {
|
||||||
res.addFrame(animation.getFrame(i), (durations!![i].toFloat() / speed).toInt())
|
res.addFrame(
|
||||||
|
animation.getFrame(i),
|
||||||
|
(durations!![i].toFloat() / speed).toInt()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
apngImageView?.setImageDrawable(res)
|
apngImageView?.setImageDrawable(res)
|
||||||
|
@ -137,12 +144,12 @@ class KotlinFragment : Fragment() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (animation == null) {
|
if ((animation == null)) {
|
||||||
ApngDecoder.decodeApngAsyncInto(
|
apngLoader?.decodeApngAsyncInto(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
imageUrls[selected],
|
imageUrls[selected],
|
||||||
apngImageView!!,
|
apngImageView!!,
|
||||||
callback = object : ApngDecoder.Callback {
|
callback = object : ApngLoader.Callback {
|
||||||
override fun onSuccess(drawable: Drawable) {
|
override fun onSuccess(drawable: Drawable) {
|
||||||
animation = (drawable as? ApngDrawable)
|
animation = (drawable as? ApngDrawable)
|
||||||
durations = IntArray(animation?.numberOfFrames ?: 0) { i ->
|
durations = IntArray(animation?.numberOfFrames ?: 0) { i ->
|
||||||
|
@ -150,13 +157,13 @@ class KotlinFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(error: Exception) {
|
override fun onError(error: Throwable) {
|
||||||
Log.e(TAG, "Error when decoding apng", error)
|
Log.e(TAG, "Error when decoding apng", error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Picasso.get().load(imageUrls[selected]).into(normalImageView)
|
normalImageView?.load(imageUrls[selected])
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent" android:layout_height="match_parent">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/delay_textInputLayout"
|
android:id="@+id/delay_textInputLayout"
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".fragments.ApngDecoderFragment">
|
tools:context=".fragments.ApngDecoderFragment"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
android:id="@+id/apngDecoderImageView"
|
android:id="@+id/apngDecoderImageView"
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:context=".fragments.JavaFragment">
|
tools:context=".fragments.JavaFragment"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
android:id="@+id/javaImageView"
|
android:id="@+id/javaImageView"
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
android:id="@+id/position_textView"
|
android:id="@+id/position_textView"
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.5.10'
|
ext.kotlin_version = '1.5.20'
|
||||||
ext.dokka_version = '1.4.3'
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
Loading…
Reference in New Issue