Working on better ApngEncoder

This commit is contained in:
oupson 2020-01-25 18:25:32 +01:00
parent acd55f6d5a
commit 90d2f33efd
9 changed files with 423 additions and 78 deletions

View File

@ -18,7 +18,7 @@
<PersistentState>
<option name="values">
<map>
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material_design_icons/navigation/ic_menu_black_24dp.xml" />
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material_design_icons/social/ic_share_black_24dp.xml" />
</map>
</option>
</PersistentState>
@ -29,7 +29,7 @@
<option name="values">
<map>
<entry key="color" value="ffffff" />
<entry key="outputName" value="ic_menu_white_24dp" />
<entry key="outputName" value="ic_share_share_24dp" />
<entry key="sourceFile" value="C:\Users\oupso\Downloads\java-seeklogo.com.svg" />
</map>
</option>

View File

@ -1,4 +1,4 @@
package oupson.apng
package oupson.apng.decoder
import android.content.Context
import android.graphics.*
@ -11,6 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import oupson.apng.*
import oupson.apng.chunks.IHDR
import oupson.apng.chunks.fcTL
import oupson.apng.exceptions.BadApng
@ -125,7 +126,12 @@ class ApngDecoder {
}
png.addAll(Utils.pngSignature.asList())
png.addAll(generateIhdr(ihdr, width, height).asList())
png.addAll(
generateIhdr(
ihdr,
width,
height
).asList())
plte?.let {
png?.addAll(it.asList())
}
@ -149,11 +155,16 @@ class ApngDecoder {
canvas.drawBitmap(buffer!!, 0f, 0f, null)
if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
canvas.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset+ decoded.width.toFloat(), yOffset + decoded.height.toFloat(), clearPaint)
canvas.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset+ decoded.width.toFloat(), yOffset + decoded.height.toFloat(),
clearPaint
)
}
canvas.drawBitmap(decoded, xOffset.toFloat(), yOffset.toFloat(), null)
drawable.addFrame(BitmapDrawable(btm), (delay / speed).toInt())
drawable.addFrame(
BitmapDrawable(
btm
), (delay / speed).toInt())
when(disposeOp) {
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
@ -165,7 +176,9 @@ class ApngDecoder {
val res = Bitmap.createBitmap(maxWidth, maxHeight, Bitmap.Config.ARGB_8888)
val can = Canvas(res)
can.drawBitmap(btm, 0f, 0f, null)
can.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset + decoded.width.toFloat(), yOffset + decoded.height.toFloat(), clearPaint)
can.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset + decoded.width.toFloat(), yOffset + decoded.height.toFloat(),
clearPaint
)
buffer = res
}
else -> buffer = btm
@ -183,7 +196,12 @@ class ApngDecoder {
val width = fcTL.pngWidth
val height = fcTL.pngHeight
png.addAll(Utils.pngSignature.asList())
png.addAll(generateIhdr(ihdr, width, height).asList())
png.addAll(
generateIhdr(
ihdr,
width,
height
).asList())
plte?.let {
png.addAll(it.asList())
}
@ -210,11 +228,16 @@ class ApngDecoder {
canvas.drawBitmap(buffer!!, 0f, 0f, null)
if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
canvas.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset+ decoded.width.toFloat(), yOffset + decoded.height.toFloat(), clearPaint)
canvas.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset+ decoded.width.toFloat(), yOffset + decoded.height.toFloat(),
clearPaint
)
}
canvas.drawBitmap(decoded, xOffset.toFloat(), yOffset.toFloat(), null)
drawable.addFrame(BitmapDrawable(btm), (delay / speed).toInt())
drawable.addFrame(
BitmapDrawable(
btm
), (delay / speed).toInt())
when(disposeOp) {
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
@ -226,7 +249,9 @@ class ApngDecoder {
val res = Bitmap.createBitmap(maxWidth, maxHeight, Bitmap.Config.ARGB_8888)
val can = Canvas(res)
can.drawBitmap(btm, 0f, 0f, null)
can.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset + decoded.width.toFloat(), yOffset + decoded.height.toFloat(), clearPaint)
can.drawRect(xOffset.toFloat(), yOffset.toFloat(), xOffset + decoded.width.toFloat(), yOffset + decoded.height.toFloat(),
clearPaint
)
buffer = res
}
else -> buffer = btm
@ -243,7 +268,13 @@ class ApngDecoder {
it.addAll(iend.asList())
it.addAll(Utils.to4Bytes(crC32.value.toInt()).asList())
inputStream.close()
return BitmapDrawable(BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size))
return BitmapDrawable(
BitmapFactory.decodeByteArray(
it.toByteArray(),
0,
it.size
)
)
}
}
}
@ -252,11 +283,12 @@ class ApngDecoder {
if (cover == null) {
cover = ArrayList()
cover.addAll(Utils.pngSignature.asList())
cover.addAll(generateIhdr(
ihdr,
maxWidth,
maxHeight
).asList())
cover.addAll(
generateIhdr(
ihdr,
maxWidth,
maxHeight
).asList())
}
// Find the chunk length
val bodySize = Utils.parseLength(byteArray.copyOfRange(i - 4, i))
@ -334,7 +366,10 @@ class ApngDecoder {
*/
@Suppress("unused")
@JvmStatic
fun decodeApng(file : File, speed: Float = 1f) : Drawable = decodeApng(FileInputStream(file), speed)
fun decodeApng(file : File, speed: Float = 1f) : Drawable =
decodeApng(
FileInputStream(file), speed
)
/**
* Decode Apng and return a Drawable who can be an [CustomAnimationDrawable] if it end successfully.
@ -346,7 +381,10 @@ class ApngDecoder {
fun decodeApng(context : Context, uri : Uri, speed: Float = 1f) : Drawable {
val inputStream = context.contentResolver.openInputStream(uri)
?: throw Exception("Failed to open InputStream, InputStream is null")
return decodeApng(inputStream, speed)
return decodeApng(
inputStream,
speed
)
}
/**
@ -357,7 +395,10 @@ class ApngDecoder {
*/
@Suppress("unused")
@JvmStatic
fun decodeApng(context : Context, @RawRes res : Int, speed: Float = 1f) : Drawable = decodeApng(context.resources.openRawResource(res), speed)
fun decodeApng(context : Context, @RawRes res : Int, speed: Float = 1f) : Drawable =
decodeApng(
context.resources.openRawResource(res), speed
)
/**
* Decode Apng and return a Drawable who can be an [CustomAnimationDrawable] if it end successfully.
@ -368,7 +409,11 @@ class ApngDecoder {
@Suppress("unused")
@JvmStatic
suspend fun decodeApng(context : Context, url : URL, speed: Float = 1f) = withContext(Dispatchers.IO) {
decodeApng(FileInputStream(Loader.load(context, url)), speed)
decodeApng(
FileInputStream(
Loader.load(context, url)
), speed
)
}
/**
@ -384,7 +429,11 @@ class ApngDecoder {
fun decodeApngAsyncInto(file : File, imageView : ImageView, speed: Float = 1f, callback: Callback? = null) {
GlobalScope.launch(Dispatchers.IO) {
try {
val drawable = decodeApng(FileInputStream(file), speed)
val drawable =
decodeApng(
FileInputStream(file),
speed
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? CustomAnimationDrawable)?.start()
@ -413,7 +462,11 @@ class ApngDecoder {
val inputStream = context.contentResolver.openInputStream(uri) ?: throw Exception("Failed to open InputStream, InputStream is null")
GlobalScope.launch(Dispatchers.IO) {
try {
val drawable = decodeApng(inputStream, speed)
val drawable =
decodeApng(
inputStream,
speed
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? CustomAnimationDrawable)?.start()
@ -441,7 +494,11 @@ class ApngDecoder {
fun decodeApngAsyncInto(context : Context, @RawRes res : Int, imageView: ImageView, speed: Float = 1f, callback: Callback? = null) {
GlobalScope.launch(Dispatchers.IO) {
try {
val drawable = decodeApng(context.resources.openRawResource(res), speed)
val drawable =
decodeApng(
context.resources.openRawResource(res),
speed
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? CustomAnimationDrawable)?.start()
@ -470,7 +527,16 @@ class ApngDecoder {
fun decodeApngAsyncInto(context : Context, url : URL, imageView: ImageView, speed: Float = 1f, callback: Callback? = null) {
GlobalScope.launch(Dispatchers.IO) {
try {
val drawable = decodeApng(FileInputStream(Loader.load(context, url)), speed)
val drawable =
decodeApng(
FileInputStream(
Loader.load(
context,
url
)
),
speed
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? CustomAnimationDrawable)?.start()
@ -499,13 +565,29 @@ class ApngDecoder {
GlobalScope.launch(Dispatchers.IO) {
try {
if (string.startsWith("http://") || string.startsWith("https://")) {
decodeApngAsyncInto(context, URL(string), imageView, speed, callback)
decodeApngAsyncInto(
context,
URL(string),
imageView,
speed,
callback
)
} 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, speed, callback)
decodeApngAsyncInto(
context,
Uri.parse(pathToLoad),
imageView,
speed,
callback
)
} else if (string.startsWith("file://android_asset/")) {
val drawable = decodeApng(context.assets.open(string.replace("file:///android_asset/", "")), speed)
val drawable =
decodeApng(
context.assets.open(string.replace("file:///android_asset/", "")),
speed
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? CustomAnimationDrawable)?.start()

View File

@ -0,0 +1,206 @@
package oupson.apng.encoder
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import oupson.apng.chunks.IDAT
import oupson.apng.imageUtils.PngEncoder
import oupson.apng.utils.Utils
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.CRC32
class ApngEncoder(
private val outputStream: OutputStream,
private val width : Int,
private val height : Int,
numberOfFrames : Int) {
private var frameIndex = 0
private var seq = 0
private val idatName : List<Byte> by lazy {
listOf(0x49.toByte(), 0x44.toByte(), 0x41.toByte(), 0x54.toByte())
}
init {
outputStream.write(Utils.pngSignature)
outputStream.write(generateIhdr())
outputStream.write(generateACTL(numberOfFrames))
}
fun writeFrame(
inputStream: InputStream,
delay: Float = 1000f,
xOffsets: Int = 0,
yOffsets: Int = 0,
blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE,
disposeOp: Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
) {
val btm = BitmapFactory.decodeStream(inputStream)
inputStream.close()
generateFCTL(btm, delay, disposeOp, blendOp, xOffsets, yOffsets)
val idat = IDAT().apply {
val byteArray = PngEncoder.encode(btm, true)
var cursor = 8
while (cursor < byteArray.size) {
val chunk = byteArray.copyOfRange(cursor, cursor + Utils.parseLength(byteArray.copyOfRange(cursor, cursor + 4)) + 12)
parse(chunk)
cursor += Utils.parseLength(byteArray.copyOfRange(cursor, cursor + 4)) + 12
}
}
idat.IDATBody.forEach { idatBody ->
if (frameIndex == 0) {
val idatChunk = ArrayList<Byte>().let { i ->
// Add IDAT
i.addAll(idatName)
// Add chunk body
i.addAll(idatBody.asList())
i.toByteArray()
}
// Add the chunk body length
outputStream.write(Utils.to4Bytes(idatBody.size))
// Generate CRC
val crc1 = CRC32()
crc1.update(idatChunk, 0, idatChunk.size)
outputStream.write(idatChunk)
outputStream.write(Utils.to4Bytes(crc1.value.toInt()))
} else {
val fdat = ArrayList<Byte>().let { fdat ->
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).asList())
// Add fdat
fdat.addAll(Utils.to4Bytes(seq++).asList())
// Add chunk body
fdat.addAll(idatBody.asList())
fdat.toByteArray()
}
// Add the chunk body length
outputStream.write(Utils.to4Bytes(idatBody.size + 4))
// Generate CRC
val crc1 = CRC32()
crc1.update(fdat, 0, fdat.size)
outputStream.write(fdat)
outputStream.write(Utils.to4Bytes(crc1.value.toInt()))
}
}
frameIndex++
}
fun writeEnd() {
// Add IEND body length : 0
outputStream.write(Utils.to4Bytes(0))
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND
val crC32 = CRC32()
crC32.update(iend, 0, iend.size)
outputStream.write(iend)
outputStream.write(Utils.to4Bytes(crC32.value.toInt()))
}
/**
* Generate the IHDR chunks.
* @return [ByteArray] The byteArray generated
*/
private fun generateIhdr(): ByteArray {
val ihdr = ArrayList<Byte>()
// We need a body var to know body length and generate crc
val ihdrBody = ArrayList<Byte>()
/**
if (((maxWidth != frames[0].width) && (maxHeight != frames[0].height)) && cover == null) {
cover = generateCover(BitmapFactory.decodeByteArray(frames[0].byteArray, 0, frames[0].byteArray.size), maxWidth!!, maxHeight!!)
}*/
// Add chunk body length
ihdr.addAll(arrayListOf(0x00, 0x00, 0x00, 0x0d))
ihdrBody.addAll(arrayListOf(0x49, 0x48, 0x44, 0x52))
// Add the max width and height
ihdrBody.addAll(Utils.to4Bytes(width).asList())
ihdrBody.addAll(Utils.to4Bytes(height).asList())
ihdrBody.add(8.toByte())
ihdrBody.add(6.toByte())
ihdrBody.add(0.toByte())
ihdrBody.add(0.toByte())
ihdrBody.add(0.toByte())
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdrBody.toByteArray(), 0, ihdrBody.size)
ihdr.addAll(ihdrBody)
ihdr.addAll(Utils.to4Bytes(crC32.value.toInt()).asList())
return ihdr.toByteArray()
}
/**
* Generate the animation control chunk
* @return [ArrayList] The byteArray generated
*/
private fun generateACTL(num: Int): ByteArray {
val res = ArrayList<Byte>()
val actl = ArrayList<Byte>()
// Add length bytes
res.addAll(arrayListOf(0, 0, 0, 0x08))
// Add acTL
actl.addAll(byteArrayOf(0x61, 0x63, 0x54, 0x4c).asList())
// Add number of frames
actl.addAll(Utils.to4Bytes(num).asList())
// Number of repeat, 0 to infinite
actl.addAll(Utils.to4Bytes(0).asList())
res.addAll(actl)
// generate crc
val crc = CRC32()
crc.update(actl.toByteArray(), 0, actl.size)
res.addAll(Utils.to4Bytes(crc.value.toInt()).asList())
return res.toByteArray()
}
private fun generateFCTL(btm : Bitmap, delay: Float, disposeOp: Utils.Companion.DisposeOp, blendOp: Utils.Companion.BlendOp, xOffsets: Int, yOffsets: Int) {
val fcTL = ArrayList<Byte>()
// Add the length of the chunk body
outputStream.write(byteArrayOf(0x00, 0x00, 0x00, 0x1A))
// Add fcTL
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).asList())
// Add the frame number
fcTL.addAll(Utils.to4Bytes(seq++).asList())
// Add width and height
fcTL.addAll(Utils.to4Bytes(btm.width).asList())
fcTL.addAll(Utils.to4Bytes(btm.height).asList())
// Add offsets
fcTL.addAll(Utils.to4Bytes(xOffsets ).asList())
fcTL.addAll(Utils.to4Bytes(yOffsets ).asList())
// Set frame delay
fcTL.addAll(Utils.to2Bytes(delay.toInt()).asList())
fcTL.addAll(Utils.to2Bytes(1000).asList())
// Add DisposeOp and BlendOp
fcTL.add(Utils.getDisposeOp(disposeOp).toByte())
fcTL.add(Utils.getBlendOp(blendOp).toByte())
// Create CRC
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
outputStream.write(fcTL.toByteArray())
outputStream.write(Utils.to4Bytes(crc.value.toInt()))
}
}

View File

@ -13,11 +13,12 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_creator.*
import oupson.apng.Apng
import oupson.apng.encoder.ApngEncoder
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
import oupson.apngcreator.adapter.ImageAdapter
import java.io.File
import java.io.FileOutputStream
class CreatorActivity : AppCompatActivity() {
companion object {
@ -35,13 +36,7 @@ class CreatorActivity : AppCompatActivity() {
val getIntent = Intent(Intent.ACTION_GET_CONTENT)
getIntent.type = "image/*"
val pickIntent = Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
pickIntent.type = "image/*"
val chooserIntent = Intent.createChooser(getIntent, "Select Image")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(pickIntent))
startActivityForResult(chooserIntent,
startActivityForResult(getIntent,
PICK_IMAGE
)
}
@ -68,38 +63,94 @@ class CreatorActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return when (item?.itemId) {
R.id.menu_create_apng -> {
// TODO
val apngCreated = Apng()
items.forEachIndexed { i, uri ->
println("delay : ${adapter?.delay?.get(i)?.toFloat() ?: 1000f}ms")
val str = contentResolver.openInputStream(uri)
apngCreated.addFrames(BitmapFactory.decodeStream(str), delay = adapter?.delay?.get(i)?.toFloat() ?: 1000f)
str?.close()
}
File(cacheDir, "apn0.png").writeBytes(apngCreated.toByteArray())
apngCreated.apply {
// TODO
//if (view.optimiseCheckBox.isChecked)
// apngCreated.optimiseFrame()
}
// TODO Open
val f = File(filesDir, "images/apng.png").apply {
if (!exists()) {
parentFile.mkdirs()
println(createNewFile())
if (items.size > 0) {
val f = File(filesDir, "images/apng.png").apply {
if (!exists()) {
parentFile.mkdirs()
println(createNewFile())
}
}
writeBytes(apngCreated.toByteArray())
val out = FileOutputStream(f)
var maxWidth = 0
var maxHeight = 0
items.forEach {
val str = contentResolver.openInputStream(it)
val btm = BitmapFactory.decodeStream(str)
if (btm.width > maxWidth)
maxWidth = btm.width
else if(btm.height > maxHeight)
maxHeight = btm.height
str?.close()
}
val encoder = ApngEncoder(out, maxWidth, maxHeight, items.size)
items.forEachIndexed { i, uri ->
println("delay : ${adapter?.delay?.get(i)?.toFloat() ?: 1000f}ms")
val str = contentResolver.openInputStream(uri) ?: return@forEachIndexed
encoder.writeFrame(
str,
delay = adapter?.delay?.get(i)?.toFloat() ?: 1000f
)
}
encoder.writeEnd()
out.close()
val intent = Intent(Intent.ACTION_VIEW)
intent.data = FileProvider.getUriForFile(
this,
"${BuildConfig.APPLICATION_ID}.provider",
f
)
startActivity(intent)
}
true
}
R.id.menu_share_apng -> {
if (items.size > 0) {
val f = File(filesDir, "images/apng.png").apply {
if (!exists()) {
parentFile.mkdirs()
println(createNewFile())
}
}
val out = FileOutputStream(f)
var maxWidth = 0
var maxHeight = 0
items.forEach {
val str = contentResolver.openInputStream(it)
val btm = BitmapFactory.decodeStream(str)
if (btm.width > maxWidth)
maxWidth = btm.width
else if(btm.height > maxHeight)
maxHeight = btm.height
str?.close()
}
val encoder = ApngEncoder(out, maxWidth, maxHeight, items.size)
items.forEachIndexed { i, uri ->
println("delay : ${adapter?.delay?.get(i)?.toFloat() ?: 1000f}ms")
val str = contentResolver.openInputStream(uri) ?: return@forEachIndexed
encoder.writeFrame(
str,
delay = adapter?.delay?.get(i)?.toFloat() ?: 1000f
)
}
val intent = Intent(Intent.ACTION_VIEW)
intent.data = FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.provider", f)
startActivity(intent)
encoder.writeEnd()
out.close()
val intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(
this@CreatorActivity,
"${BuildConfig.APPLICATION_ID}.provider",
f
))
type = "image/png"
}
startActivity(Intent.createChooser(intent, resources.getText(R.string.share)))
}
true
}
else -> super.onOptionsItemSelected(item)

View File

@ -2,17 +2,16 @@ package oupson.apngcreator.activities
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import oupson.apng.ApngDecoder
import oupson.apng.CustomAnimationDrawable
import oupson.apng.decoder.ApngDecoder
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
class ViewerActivity : AppCompatActivity() {
@ -42,15 +41,13 @@ class ViewerActivity : AppCompatActivity() {
private fun load() {
val uri = intent.data ?: return
GlobalScope.launch(Dispatchers.IO) {
//val animator = imageView.loadApng(uri, null)
val drawable = ApngDecoder.decodeApng(this@ViewerActivity, uri)
GlobalScope.launch(Dispatchers.Main) {
viewerImageView.setImageDrawable(drawable)
if (drawable is CustomAnimationDrawable)
drawable.start()
ApngDecoder.decodeApngAsyncInto(this, uri, viewerImageView, callback = object : ApngDecoder.Callback {
override fun onSuccess(drawable: Drawable) {}
override fun onError(error: Exception) {
if (BuildConfig.DEBUG)
Log.e("ViewerActivity", "Error when loading file", error)
}
}
})
}
override fun onRequestPermissionsResult(requestCode: Int,

View File

@ -9,7 +9,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.fragment.app.Fragment
import oupson.apng.ApngDecoder
import oupson.apng.decoder.ApngDecoder
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
import java.net.URL

View File

@ -14,7 +14,7 @@ import androidx.fragment.app.Fragment;
import org.jetbrains.annotations.NotNull;
import oupson.apng.ApngDecoder;
import oupson.apng.decoder.ApngDecoder;
import oupson.apngcreator.R;

View File

@ -8,4 +8,11 @@
android:title="@string/create"
app:showAsAction="ifRoom"
app:iconTint="@color/control"/>
<item
android:id="@+id/menu_share_apng"
android:icon="@drawable/ic_share_share_24dp"
android:title="@string/share"
app:showAsAction="ifRoom"
app:iconTint="@color/control"/>
</menu>

View File

@ -11,6 +11,8 @@
<string name="menu_title_apng_decoder_fragment">ApngDecoder</string>
<string name="create">Create</string>
<string name="share">Share</string>
<string name="select_image">Select an image</string>
<string name="open">open</string>
<string name="close">close</string>