Merge branch 'coroutine'

This commit is contained in:
oupson 2022-05-24 19:48:02 +02:00
commit 11a5ba6005
21 changed files with 1243 additions and 1033 deletions

View File

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

View File

@ -1,2 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="oupson.apng"> <manifest package="oupson.apng">
</manifest> </manifest>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
} }
} }
encoder.writeEnd() withContext(Dispatchers.IO) {
encoder.writeEnd()
}
if (builder != null) { if (builder != null) {
@ -423,71 +489,14 @@ 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()
val deleteResult = File(filesDir, "images").deleteRecursively() lifecycleScope.launch(Dispatchers.IO) {
if (BuildConfig.DEBUG) val deleteResult = File(filesDir, "images").deleteRecursively()
Log.v(TAG, "Deleted images dir : $deleteResult") if (BuildConfig.DEBUG)
Log.v(TAG, "Deleted images dir : $deleteResult")
}
} }
inner class SwipeToDeleteCallback(private val adapter: ImageAdapter) : inner class SwipeToDeleteCallback(private val adapter: ImageAdapter) :

View File

@ -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
val httpCacheDir = File(cacheDir, "http") lifecycleScope.launch(Dispatchers.IO) {
HttpResponseCache.install(httpCacheDir, httpCacheSize) val httpCacheDir = File(cacheDir, "http")
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,35 +175,41 @@ 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()
HttpResponseCache.getInstalled()?.flush() lifecycleScope.launch(Dispatchers.IO) {
HttpResponseCache.getInstalled()?.flush()
}
} }
private fun setUpBottomAppBarShapeAppearance() { private fun setUpBottomAppBarShapeAppearance() {
val fabShapeAppearanceModel: ShapeAppearanceModel = fabCreate.shapeAppearanceModel if (binding != null) {
val cutCornersFab = val fabShapeAppearanceModel: ShapeAppearanceModel =
(fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment binding!!.fabCreate.shapeAppearanceModel
&& fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment) val cutCornersFab =
val topEdge = (fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment
if (cutCornersFab) BottomAppBarCutCornersTopEdge( && fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment)
bottomAppBar.fabCradleMargin, val topEdge =
bottomAppBar.fabCradleRoundedCornerRadius, if (cutCornersFab) BottomAppBarCutCornersTopEdge(
bottomAppBar.cradleVerticalOffset binding!!.bottomAppBar.fabCradleMargin,
) else BottomAppBarTopEdgeTreatment( binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
bottomAppBar.fabCradleMargin, binding!!.bottomAppBar.cradleVerticalOffset
bottomAppBar.fabCradleRoundedCornerRadius, ) else BottomAppBarTopEdgeTreatment(
bottomAppBar.cradleVerticalOffset binding!!.bottomAppBar.fabCradleMargin,
) binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
val babBackground = bottomAppBar.background as MaterialShapeDrawable binding!!.bottomAppBar.cradleVerticalOffset
babBackground.shapeAppearanceModel = )
babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build() val babBackground = binding!!.bottomAppBar.background as MaterialShapeDrawable
babBackground.shapeAppearanceModel =
babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build()
}
} }

View File

@ -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 {
override fun onSuccess(drawable: Drawable) {} if (binding != null)
override fun onError(error: Exception) { apngLoader?.decodeApngAsyncInto(
Log.e("ViewerActivity", "Error when loading file", error) this,
} uri,
}, ApngDecoder.Config(decodeCoverFrame = false)) binding!!.viewerImageView,
callback = object : ApngLoader.Callback {
override fun onSuccess(drawable: Drawable) {}
override fun onError(error: Throwable) {
Log.e("ViewerActivity", "Error when loading file", error)
}
},
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 -> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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