Work on app
This commit is contained in:
parent
31c7529d45
commit
21f9512688
|
@ -1,6 +1,5 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
|
||||
android {
|
||||
|
@ -29,8 +28,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="oupson.apng">
|
||||
</manifest>
|
||||
|
|
|
@ -71,6 +71,7 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
|||
val inputStream = BufferedInputStream(inputStream)
|
||||
val bytes = ByteArray(8)
|
||||
inputStream.mark(8)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
inputStream.read(bytes)
|
||||
}
|
||||
|
@ -108,7 +109,6 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
|||
if (withContext(Dispatchers.IO) {
|
||||
byteRead = inputStream.read(lengthChunk)
|
||||
|
||||
|
||||
if (byteRead != -1) {
|
||||
length = Utils.uIntFromBytesBigEndian(lengthChunk)
|
||||
|
||||
|
@ -149,6 +149,7 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
|||
)
|
||||
}
|
||||
}
|
||||
cover?.close()
|
||||
cover = null
|
||||
} else {
|
||||
// Add IEND body length : 0
|
||||
|
@ -234,9 +235,9 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
|||
}
|
||||
else -> buffer = btm
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
png?.close()
|
||||
png = ByteArrayOutputStream(4096)
|
||||
|
||||
// Parse Frame ConTroL chunk
|
||||
|
@ -318,6 +319,7 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
|||
)
|
||||
|
||||
val pngBytes = png.toByteArray()
|
||||
png.close()
|
||||
val decoded = BitmapFactory.decodeByteArray(
|
||||
pngBytes,
|
||||
0,
|
||||
|
@ -393,11 +395,14 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
|||
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
||||
it.write(Utils.IEND)
|
||||
it.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
inputStream.close()
|
||||
}
|
||||
|
||||
val pngBytes = it.toByteArray()
|
||||
it.close()
|
||||
|
||||
return@withContext BitmapDrawable(
|
||||
context.resources,
|
||||
BitmapFactory.decodeByteArray(
|
||||
|
@ -530,16 +535,14 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
|||
|
||||
suspend fun getDecoded(context: Context): Result<Drawable> {
|
||||
if (result == null) {
|
||||
result =
|
||||
decodeApng(context)
|
||||
|
||||
result = decodeApng(context)
|
||||
|
||||
kotlin.runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
inputStream?.close()
|
||||
}
|
||||
}.onFailure {
|
||||
return Result.failure(it)
|
||||
this.result = Result.failure(it)
|
||||
}
|
||||
|
||||
inputStream = null
|
||||
|
|
|
@ -10,9 +10,9 @@ 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.io.FileNotFoundException
|
||||
import java.net.URL
|
||||
|
||||
class ApngLoader(parent: Job? = null) {
|
||||
|
@ -50,18 +50,18 @@ class ApngLoader(parent: Job? = null) {
|
|||
file: File,
|
||||
imageView: ImageView,
|
||||
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||
): Result<Drawable> {
|
||||
val result =
|
||||
ApngDecoder(
|
||||
): Result<Drawable> =
|
||||
kotlin.runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
FileInputStream(file)
|
||||
},
|
||||
}
|
||||
}.mapResult { input ->
|
||||
ApngDecoder(
|
||||
input,
|
||||
config
|
||||
).getDecoded(context)
|
||||
|
||||
if (result.isSuccess) {
|
||||
}.onSuccess { drawable ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val drawable = result.getOrNull()
|
||||
imageView.setImageDrawable(drawable)
|
||||
(drawable as? AnimationDrawable)?.start()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
|
@ -69,8 +69,6 @@ class ApngLoader(parent: Job? = null) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Apng into an imageView.
|
||||
|
@ -84,19 +82,16 @@ class ApngLoader(parent: Job? = null) {
|
|||
uri: Uri,
|
||||
imageView: ImageView,
|
||||
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||
): Result<Drawable> {
|
||||
val inputStream =
|
||||
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
|
||||
?: throw FileNotFoundException("Failed to load $uri") // TODO Result
|
||||
val result =
|
||||
): Result<Drawable> =
|
||||
kotlin.runCatching {
|
||||
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }!!
|
||||
}.mapResult { inputStream ->
|
||||
ApngDecoder(
|
||||
inputStream,
|
||||
config
|
||||
).getDecoded(context)
|
||||
|
||||
if (result.isSuccess) {
|
||||
}.onSuccess { drawable ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val drawable = result.getOrNull()
|
||||
imageView.setImageDrawable(drawable)
|
||||
(drawable as? AnimationDrawable)?.start()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
|
@ -104,8 +99,7 @@ class ApngLoader(parent: Job? = null) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load Apng into an imageView.
|
||||
|
@ -118,18 +112,15 @@ class ApngLoader(parent: Job? = null) {
|
|||
context: Context, @RawRes res: Int,
|
||||
imageView: ImageView,
|
||||
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||
): Result<Drawable> {
|
||||
val result =
|
||||
): Result<Drawable> =
|
||||
ApngDecoder(
|
||||
withContext(Dispatchers.IO) {
|
||||
context.resources.openRawResource(res)
|
||||
},
|
||||
config
|
||||
).getDecoded(context)
|
||||
|
||||
if (result.isSuccess) {
|
||||
.onSuccess { drawable ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val drawable = result.getOrNull()
|
||||
imageView.setImageDrawable(drawable)
|
||||
(drawable as? AnimationDrawable)?.start()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
|
@ -137,8 +128,7 @@ class ApngLoader(parent: Job? = null) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load Apng into an imageView, asynchronously.
|
||||
|
@ -152,13 +142,11 @@ class ApngLoader(parent: Job? = null) {
|
|||
url: URL,
|
||||
imageView: ImageView,
|
||||
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||
): Result<Drawable> {
|
||||
val result =
|
||||
): Result<Drawable> =
|
||||
ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) }
|
||||
.getDecoded(context)
|
||||
if (result.isSuccess) {
|
||||
.onSuccess { drawable ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val drawable = result.getOrNull()
|
||||
imageView.setImageDrawable(drawable)
|
||||
(drawable as? AnimationDrawable)?.start()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
|
@ -167,9 +155,6 @@ class ApngLoader(parent: Job? = null) {
|
|||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Apng into an imageView, asynchronously.
|
||||
* @param context Context needed for decoding the image and creating the animation drawable.
|
||||
|
@ -185,12 +170,16 @@ class ApngLoader(parent: Job? = null) {
|
|||
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||
): Result<Drawable> {
|
||||
return if (string.startsWith("http://") || string.startsWith("https://")) {
|
||||
kotlin.runCatching { URL(string) }
|
||||
.mapResult { url ->
|
||||
decodeApngInto(
|
||||
context,
|
||||
URL(string),
|
||||
url,
|
||||
imageView,
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
} else if (File(string).exists()) {
|
||||
var pathToLoad =
|
||||
if (string.startsWith("content://")) string else "file://$string"
|
||||
|
@ -202,17 +191,14 @@ class ApngLoader(parent: Job? = null) {
|
|||
config
|
||||
)
|
||||
} else if (string.startsWith("file://android_asset/")) {
|
||||
val inputStream = kotlin.runCatching {
|
||||
kotlin.runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
context.assets.open(string.replace("file:///android_asset/", ""))
|
||||
}
|
||||
}.getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
val result = ApngDecoder(inputStream, config).getDecoded(context)
|
||||
if (result.isSuccess) {
|
||||
.mapResult { inputStream -> ApngDecoder(inputStream, config).getDecoded(context) }
|
||||
.onSuccess { drawable ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val drawable = result.getOrNull()
|
||||
imageView.setImageDrawable(drawable)
|
||||
(drawable as? AnimationDrawable)?.start()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
|
@ -220,7 +206,6 @@ class ApngLoader(parent: Job? = null) {
|
|||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
} else {
|
||||
throw Exception("Cannot open string")
|
||||
}
|
||||
|
|
|
@ -291,7 +291,7 @@ class ApngEncoder(
|
|||
) {
|
||||
if (currentFrame == 0) {
|
||||
if (btm.width != width || btm.height != height)
|
||||
throw InvalidFrameSizeException(
|
||||
throw InvalidFrameSizeException( // TODO
|
||||
btm.width,
|
||||
btm.height,
|
||||
width,
|
||||
|
|
|
@ -24,8 +24,11 @@ class Loader {
|
|||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.useCaches = true
|
||||
connection.connect()
|
||||
|
||||
val inputStream = connection.inputStream
|
||||
|
||||
if (connection.responseCode == 200) {
|
||||
val input = BufferedInputStream(connection.inputStream)
|
||||
val input = BufferedInputStream(inputStream)
|
||||
val output = ByteArrayOutputStream()
|
||||
var bytesRead: Int
|
||||
val buffer = ByteArray(4096)
|
||||
|
@ -36,9 +39,13 @@ class Loader {
|
|||
} while (bytesRead != -1)
|
||||
input.close()
|
||||
output.close()
|
||||
|
||||
inputStream.close()
|
||||
connection.disconnect()
|
||||
|
||||
output.toByteArray()
|
||||
} else {
|
||||
inputStream.close()
|
||||
connection.disconnect()
|
||||
throw Exception("Error when downloading file : ${connection.responseCode}")
|
||||
}
|
||||
|
|
|
@ -330,5 +330,14 @@ class Utils {
|
|||
}
|
||||
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: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
|
@ -24,29 +23,35 @@ android {
|
|||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.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'
|
||||
androidTestImplementation 'androidx.test:runner:1.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 fileTree(include: ['*.aar'], dir: 'libs')
|
||||
|
|
|
@ -14,18 +14,18 @@ import android.util.Log
|
|||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_creator.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import oupson.apng.encoder.ApngEncoder
|
||||
|
@ -33,6 +33,7 @@ import oupson.apng.utils.Utils
|
|||
import oupson.apngcreator.BuildConfig
|
||||
import oupson.apngcreator.R
|
||||
import oupson.apngcreator.adapter.ImageAdapter
|
||||
import oupson.apngcreator.databinding.ActivityCreatorBinding
|
||||
import oupson.apngcreator.dialogs.DelayInputDialog
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
@ -42,8 +43,6 @@ import kotlin.collections.ArrayList
|
|||
|
||||
class CreatorActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val PICK_IMAGE = 1
|
||||
private const val WRITE_REQUEST_CODE = 2
|
||||
private const val TAG = "CreatorActivity"
|
||||
|
||||
private const val CREATION_CHANNEL_ID =
|
||||
|
@ -57,35 +56,97 @@ class CreatorActivity : AppCompatActivity() {
|
|||
|
||||
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?) {
|
||||
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)
|
||||
getIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
getIntent.type = "image/*"
|
||||
|
||||
startActivityForResult(
|
||||
getIntent,
|
||||
PICK_IMAGE
|
||||
)
|
||||
pickLauncher.launch(getIntent)
|
||||
}
|
||||
|
||||
adapter = ImageAdapter(this, items)
|
||||
adapter = ImageAdapter(this, items, lifecycleScope)
|
||||
adapter?.setHasStableIds(true)
|
||||
|
||||
imageRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
imageRecyclerView.setHasFixedSize(true)
|
||||
imageRecyclerView.itemAnimator = object : DefaultItemAnimator() {
|
||||
binding?.imageRecyclerView?.layoutManager = LinearLayoutManager(this)
|
||||
binding?.imageRecyclerView?.setHasFixedSize(true)
|
||||
binding?.imageRecyclerView?.itemAnimator = object : DefaultItemAnimator() {
|
||||
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
imageRecyclerView.setItemViewCacheSize(20)
|
||||
binding?.imageRecyclerView?.setItemViewCacheSize(20)
|
||||
if (adapter != null)
|
||||
ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(imageRecyclerView)
|
||||
ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(binding?.imageRecyclerView)
|
||||
|
||||
adapter?.clickListener = { position ->
|
||||
DelayInputDialog(object : DelayInputDialog.InputSenderDialogListener {
|
||||
|
@ -104,8 +165,8 @@ class CreatorActivity : AppCompatActivity() {
|
|||
}, items[position].second).show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
setSupportActionBar(creatorBottomAppBar)
|
||||
imageRecyclerView.adapter = adapter
|
||||
setSupportActionBar(binding?.creatorBottomAppBar)
|
||||
binding?.imageRecyclerView?.adapter = adapter
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
@ -148,7 +209,7 @@ class CreatorActivity : AppCompatActivity() {
|
|||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val randomFileName = UUID.randomUUID().toString()
|
||||
val f = File(filesDir, "images/$randomFileName.png").apply {
|
||||
if (!exists()) {
|
||||
|
@ -205,7 +266,7 @@ class CreatorActivity : AppCompatActivity() {
|
|||
setSmallIcon(R.drawable.ic_create_white_24dp)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val randomFileName = UUID.randomUUID().toString()
|
||||
val f = File(filesDir, "images/$randomFileName.png").apply {
|
||||
if (!exists()) {
|
||||
|
@ -272,7 +333,10 @@ class CreatorActivity : AppCompatActivity() {
|
|||
type = "image/png"
|
||||
putExtra(Intent.EXTRA_TITLE, "${items[0].first.lastPathSegment}.png")
|
||||
}
|
||||
startActivityForResult(intent, WRITE_REQUEST_CODE)
|
||||
|
||||
writeLauncher.launch(
|
||||
intent
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
@ -409,7 +473,9 @@ class CreatorActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
encoder.writeEnd()
|
||||
}
|
||||
|
||||
|
||||
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() {
|
||||
super.onDestroy()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val deleteResult = File(filesDir, "images").deleteRecursively()
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.v(TAG, "Deleted images dir : $deleteResult")
|
||||
}
|
||||
}
|
||||
|
||||
inner class SwipeToDeleteCallback(private val adapter: ImageAdapter) :
|
||||
ItemTouchHelper.SimpleCallback(
|
||||
|
|
|
@ -4,19 +4,25 @@ import android.annotation.SuppressLint
|
|||
import android.content.Intent
|
||||
import android.net.http.HttpResponseCache
|
||||
import android.os.Bundle
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.bottomappbar.BottomAppBarTopEdgeTreatment
|
||||
import com.google.android.material.shape.CutCornerTreatment
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
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.R
|
||||
import oupson.apngcreator.databinding.ActivityMainBinding
|
||||
import oupson.apngcreator.fragments.ApngDecoderFragment
|
||||
import oupson.apngcreator.fragments.JavaFragment
|
||||
import oupson.apngcreator.fragments.KotlinFragment
|
||||
|
@ -28,37 +34,66 @@ class MainActivity : AppCompatActivity() {
|
|||
private const val TAG = "MainActivity"
|
||||
}
|
||||
|
||||
private var binding: ActivityMainBinding? = null
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
val httpCacheSize = 10 * 1024 * 1024.toLong() // 10 MiB
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val httpCacheDir = File(cacheDir, "http")
|
||||
HttpResponseCache.install(httpCacheDir, httpCacheSize)
|
||||
}
|
||||
|
||||
fabCreate.setOnClickListener {
|
||||
binding?.fabCreate?.setOnClickListener {
|
||||
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.close
|
||||
)
|
||||
drawer_layout.addDrawerListener(drawerToggle)
|
||||
binding?.drawerLayout?.addDrawerListener(drawerToggle)
|
||||
drawerToggle.syncState()
|
||||
|
||||
var selected = 0
|
||||
|
||||
navigationView.setNavigationItemSelectedListener { menuItem : MenuItem ->
|
||||
binding?.navigationView?.setNavigationItemSelectedListener { menuItem: MenuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_kotlin_fragment -> {
|
||||
if (selected != 0) {
|
||||
|
@ -98,7 +133,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
drawer_layout.closeDrawer(GravityCompat.START)
|
||||
binding?.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
|
||||
return@setNavigationItemSelectedListener true
|
||||
}
|
||||
|
@ -109,9 +144,10 @@ class MainActivity : AppCompatActivity() {
|
|||
supportFragmentManager.beginTransaction().apply {
|
||||
add(
|
||||
R.id.fragment_container,
|
||||
KotlinFragment.newInstance(), "KotlinFragment")
|
||||
KotlinFragment.newInstance(), "KotlinFragment"
|
||||
)
|
||||
}.commit()
|
||||
navigationView.setCheckedItem(R.id.menu_kotlin_fragment)
|
||||
binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment)
|
||||
selected = 0
|
||||
}
|
||||
"java" -> {
|
||||
|
@ -121,7 +157,7 @@ class MainActivity : AppCompatActivity() {
|
|||
JavaFragment()
|
||||
)
|
||||
}.commit()
|
||||
navigationView.setCheckedItem(R.id.menu_java_fragment)
|
||||
binding?.navigationView?.setCheckedItem(R.id.menu_java_fragment)
|
||||
selected = 1
|
||||
}
|
||||
"apng_decoder" -> {
|
||||
|
@ -131,7 +167,7 @@ class MainActivity : AppCompatActivity() {
|
|||
ApngDecoderFragment.newInstance()
|
||||
)
|
||||
}.commit()
|
||||
navigationView.setCheckedItem(R.id.menu_apng_decoder_fragment)
|
||||
binding?.navigationView?.setCheckedItem(R.id.menu_apng_decoder_fragment)
|
||||
selected = 2
|
||||
}
|
||||
}
|
||||
|
@ -139,36 +175,42 @@ class MainActivity : AppCompatActivity() {
|
|||
supportFragmentManager.beginTransaction().apply {
|
||||
add(
|
||||
R.id.fragment_container,
|
||||
KotlinFragment.newInstance(), "KotlinFragment")
|
||||
KotlinFragment.newInstance(), "KotlinFragment"
|
||||
)
|
||||
}.commit()
|
||||
navigationView.setCheckedItem(R.id.menu_kotlin_fragment)
|
||||
binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
HttpResponseCache.getInstalled()?.flush()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpBottomAppBarShapeAppearance() {
|
||||
val fabShapeAppearanceModel: ShapeAppearanceModel = fabCreate.shapeAppearanceModel
|
||||
if (binding != null) {
|
||||
val fabShapeAppearanceModel: ShapeAppearanceModel =
|
||||
binding!!.fabCreate.shapeAppearanceModel
|
||||
val cutCornersFab =
|
||||
(fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment
|
||||
&& fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment)
|
||||
val topEdge =
|
||||
if (cutCornersFab) BottomAppBarCutCornersTopEdge(
|
||||
bottomAppBar.fabCradleMargin,
|
||||
bottomAppBar.fabCradleRoundedCornerRadius,
|
||||
bottomAppBar.cradleVerticalOffset
|
||||
binding!!.bottomAppBar.fabCradleMargin,
|
||||
binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
|
||||
binding!!.bottomAppBar.cradleVerticalOffset
|
||||
) else BottomAppBarTopEdgeTreatment(
|
||||
bottomAppBar.fabCradleMargin,
|
||||
bottomAppBar.fabCradleRoundedCornerRadius,
|
||||
bottomAppBar.cradleVerticalOffset
|
||||
binding!!.bottomAppBar.fabCradleMargin,
|
||||
binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
|
||||
binding!!.bottomAppBar.cradleVerticalOffset
|
||||
)
|
||||
val babBackground = bottomAppBar.background as MaterialShapeDrawable
|
||||
val babBackground = binding!!.bottomAppBar.background as MaterialShapeDrawable
|
||||
babBackground.shapeAppearanceModel =
|
||||
babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class BottomAppBarCutCornersTopEdge(
|
||||
|
|
|
@ -9,16 +9,19 @@ import android.view.View
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.android.synthetic.main.activity_viewer.*
|
||||
import oupson.apng.decoder.ApngDecoder
|
||||
import oupson.apng.decoder.ApngLoader
|
||||
import oupson.apngcreator.R
|
||||
import oupson.apngcreator.databinding.ActivityViewerBinding
|
||||
|
||||
class ViewerActivity : AppCompatActivity() {
|
||||
private var apngLoader: ApngLoader? = null
|
||||
private var binding: ActivityViewerBinding? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_viewer)
|
||||
binding = ActivityViewerBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding?.root)
|
||||
|
||||
this.apngLoader = ApngLoader()
|
||||
|
||||
|
@ -54,10 +57,12 @@ class ViewerActivity : AppCompatActivity() {
|
|||
|
||||
private fun load() {
|
||||
val uri = intent.data ?: return
|
||||
|
||||
if (binding != null)
|
||||
apngLoader?.decodeApngAsyncInto(
|
||||
this,
|
||||
uri,
|
||||
viewerImageView,
|
||||
binding!!.viewerImageView,
|
||||
callback = object : ApngLoader.Callback {
|
||||
override fun onSuccess(drawable: Drawable) {}
|
||||
override fun onError(error: Throwable) {
|
||||
|
|
|
@ -10,13 +10,17 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import oupson.apngcreator.R
|
||||
|
||||
class ImageAdapter(private val context : Context, private val list : List<Triple<Uri, Int, Long>>) : RecyclerView.Adapter<ImageAdapter.ImageHolder>() {
|
||||
class ImageAdapter(
|
||||
private val context: Context,
|
||||
private val list: List<Triple<Uri, Int, Long>>,
|
||||
private val scope: CoroutineScope
|
||||
) : RecyclerView.Adapter<ImageAdapter.ImageHolder>() {
|
||||
inner class ImageHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val imageView: ImageView? = view.findViewById(R.id.listImageView)
|
||||
val textDelay: TextView? = view.findViewById(R.id.textDelay)
|
||||
|
@ -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.positionTextView?.text = String.format("# %03d", holder.adapterPosition + 1)
|
||||
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 btm =
|
||||
BitmapFactory.decodeStream(inputStream, null, BitmapFactory.Options().apply {
|
||||
|
|
|
@ -11,8 +11,7 @@ import android.widget.Button
|
|||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.android.synthetic.main.activity_creator.*
|
||||
import coil.load
|
||||
import oupson.apng.decoder.ApngLoader
|
||||
import oupson.apng.drawable.ApngDrawable
|
||||
import oupson.apngcreator.BuildConfig
|
||||
|
@ -145,7 +144,7 @@ class KotlinFragment : Fragment() {
|
|||
}
|
||||
})
|
||||
|
||||
if (animation == null) {
|
||||
if ((animation == null)) {
|
||||
apngLoader?.decodeApngAsyncInto(
|
||||
requireContext(),
|
||||
imageUrls[selected],
|
||||
|
@ -158,13 +157,13 @@ class KotlinFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onError(error: Exception) {
|
||||
override fun onError(error: Throwable) {
|
||||
Log.e(TAG, "Error when decoding apng", error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Picasso.get().load(imageUrls[selected]).into(normalImageView)
|
||||
normalImageView?.load(imageUrls[selected])
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:id="@+id/delay_textInputLayout"
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.ApngDecoderFragment">
|
||||
tools:context=".fragments.ApngDecoderFragment"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/apngDecoderImageView"
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".fragments.JavaFragment">
|
||||
tools:context=".fragments.JavaFragment"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/javaImageView"
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
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.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.5.10'
|
||||
ext.dokka_version = '1.4.3'
|
||||
ext.kotlin_version = '1.5.20'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
Loading…
Reference in New Issue