fix: 使用图片选择器并支持九宫格高亮背景
This commit is contained in:
@@ -6,6 +6,7 @@ import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -165,14 +166,14 @@ fun ReadBookRouteScreen(
|
||||
}
|
||||
|
||||
val readStyleImagePicker = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
ActivityResultContracts.PickVisualMedia()
|
||||
) { uri ->
|
||||
uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleImageSelected(it)) }
|
||||
}
|
||||
|
||||
var pendingReadStyleImageIsNight by remember { mutableStateOf(false) }
|
||||
val readStyleImagePickerForMode = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
ActivityResultContracts.PickVisualMedia()
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
viewModel.onIntent(ReadBookIntent.ReadStyleImageSelectedForMode(it, pendingReadStyleImageIsNight))
|
||||
@@ -367,11 +368,15 @@ fun ReadBookRouteScreen(
|
||||
booksDirPicker.launch(null)
|
||||
}
|
||||
is ReadBookEffect.OpenReadStyleImagePicker -> {
|
||||
readStyleImagePicker.launch(arrayOf("image/*"))
|
||||
readStyleImagePicker.launch(
|
||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
)
|
||||
}
|
||||
is ReadBookEffect.OpenReadStyleImagePickerForMode -> {
|
||||
pendingReadStyleImageIsNight = effect.isNight
|
||||
readStyleImagePickerForMode.launch(arrayOf("image/*"))
|
||||
readStyleImagePickerForMode.launch(
|
||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
)
|
||||
}
|
||||
is ReadBookEffect.OpenReadStyleImport -> {
|
||||
readStyleImportPicker.launch(
|
||||
|
||||
@@ -527,9 +527,12 @@ data class TextLine(
|
||||
val sh = rectHeight * scale
|
||||
val dx = startX + (rectWidth - sw) / 2f
|
||||
val dy = top + (rectHeight - sh) / 2f
|
||||
val destination = android.graphics.RectF(dx, dy, dx + sw, dy + sh)
|
||||
canvas.save()
|
||||
canvas.clipRect(startX, top, endX, bottom)
|
||||
canvas.drawBitmap(bitmap, null, android.graphics.RectF(dx, dy, dx + sw, dy + sh), paint)
|
||||
if (!drawNinePatchBitmap(canvas, bitmap, destination, paint, bgImage)) {
|
||||
canvas.drawBitmap(bitmap, null, destination, paint)
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
2 -> {
|
||||
@@ -565,6 +568,124 @@ data class TextLine(
|
||||
PaintPool.recycle(paint)
|
||||
}
|
||||
|
||||
private fun drawNinePatchBitmap(
|
||||
canvas: Canvas,
|
||||
bitmap: Bitmap,
|
||||
destination: android.graphics.RectF,
|
||||
paint: Paint,
|
||||
path: String,
|
||||
): Boolean {
|
||||
bitmap.ninePatchChunk?.takeIf(android.graphics.NinePatch::isNinePatchChunk)?.let { chunk ->
|
||||
android.graphics.NinePatch(bitmap, chunk, null).draw(
|
||||
canvas,
|
||||
android.graphics.Rect(
|
||||
destination.left.toInt(),
|
||||
destination.top.toInt(),
|
||||
destination.right.toInt(),
|
||||
destination.bottom.toInt(),
|
||||
),
|
||||
paint,
|
||||
)
|
||||
return true
|
||||
}
|
||||
if (!isRawNinePatchPath(path) || bitmap.width < 3 || bitmap.height < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
val horizontalStretch = findNinePatchStretchRange(bitmap, horizontal = true) ?: return false
|
||||
val verticalStretch = findNinePatchStretchRange(bitmap, horizontal = false) ?: return false
|
||||
val sourceX = intArrayOf(
|
||||
1,
|
||||
horizontalStretch.first,
|
||||
horizontalStretch.last + 1,
|
||||
bitmap.width - 1,
|
||||
)
|
||||
val sourceY = intArrayOf(
|
||||
1,
|
||||
verticalStretch.first,
|
||||
verticalStretch.last + 1,
|
||||
bitmap.height - 1,
|
||||
)
|
||||
|
||||
var leftWidth = (sourceX[1] - sourceX[0]).toFloat()
|
||||
var rightWidth = (sourceX[3] - sourceX[2]).toFloat()
|
||||
val fixedWidth = leftWidth + rightWidth
|
||||
if (fixedWidth > destination.width() && fixedWidth > 0f) {
|
||||
val ratio = destination.width() / fixedWidth
|
||||
leftWidth *= ratio
|
||||
rightWidth *= ratio
|
||||
}
|
||||
|
||||
var topHeight = (sourceY[1] - sourceY[0]).toFloat()
|
||||
var bottomHeight = (sourceY[3] - sourceY[2]).toFloat()
|
||||
val fixedHeight = topHeight + bottomHeight
|
||||
if (fixedHeight > destination.height() && fixedHeight > 0f) {
|
||||
val ratio = destination.height() / fixedHeight
|
||||
topHeight *= ratio
|
||||
bottomHeight *= ratio
|
||||
}
|
||||
|
||||
val destinationX = floatArrayOf(
|
||||
destination.left,
|
||||
destination.left + leftWidth,
|
||||
destination.right - rightWidth,
|
||||
destination.right,
|
||||
)
|
||||
val destinationY = floatArrayOf(
|
||||
destination.top,
|
||||
destination.top + topHeight,
|
||||
destination.bottom - bottomHeight,
|
||||
destination.bottom,
|
||||
)
|
||||
|
||||
for (row in 0..2) {
|
||||
for (column in 0..2) {
|
||||
if (sourceX[column] == sourceX[column + 1] ||
|
||||
sourceY[row] == sourceY[row + 1] ||
|
||||
destinationX[column] == destinationX[column + 1] ||
|
||||
destinationY[row] == destinationY[row + 1]
|
||||
) {
|
||||
continue
|
||||
}
|
||||
canvas.drawBitmap(
|
||||
bitmap,
|
||||
android.graphics.Rect(
|
||||
sourceX[column],
|
||||
sourceY[row],
|
||||
sourceX[column + 1],
|
||||
sourceY[row + 1],
|
||||
),
|
||||
android.graphics.RectF(
|
||||
destinationX[column],
|
||||
destinationY[row],
|
||||
destinationX[column + 1],
|
||||
destinationY[row + 1],
|
||||
),
|
||||
paint,
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun findNinePatchStretchRange(bitmap: Bitmap, horizontal: Boolean): IntRange? {
|
||||
val limit = if (horizontal) bitmap.width - 2 else bitmap.height - 2
|
||||
var first = -1
|
||||
var last = -1
|
||||
for (position in 1..limit) {
|
||||
val color = if (horizontal) bitmap.getPixel(position, 0) else bitmap.getPixel(0, position)
|
||||
val isMarker = android.graphics.Color.alpha(color) == 0xFF &&
|
||||
android.graphics.Color.red(color) == 0 &&
|
||||
android.graphics.Color.green(color) == 0 &&
|
||||
android.graphics.Color.blue(color) == 0
|
||||
if (isMarker) {
|
||||
if (first < 0) first = position
|
||||
last = position
|
||||
}
|
||||
}
|
||||
return if (first >= 0) first..last else null
|
||||
}
|
||||
|
||||
fun checkFastDraw(): Boolean {
|
||||
if (!ReadConfig.optimizeRender || exceed || !onlyTextColumn || textPage.isMsgPage) {
|
||||
return false
|
||||
@@ -664,25 +785,26 @@ data class TextLine(
|
||||
private fun loadBgBitmap(path: String): Bitmap? {
|
||||
return try {
|
||||
val ctx = appCtx
|
||||
val sampleBitmap = !isRawNinePatchPath(path)
|
||||
if (path.startsWith("assets://")) {
|
||||
val assetPath = path.removePrefix("assets://")
|
||||
ctx.assets.open(assetPath).use { input ->
|
||||
decodeSampledBitmap(input)
|
||||
decodeSampledBitmap(input, sampleBitmap)
|
||||
}
|
||||
} else if (path.startsWith("content://")) {
|
||||
val uri = android.net.Uri.parse(path)
|
||||
ctx.contentResolver.openInputStream(uri)?.use { input ->
|
||||
decodeSampledBitmap(input)
|
||||
decodeSampledBitmap(input, sampleBitmap)
|
||||
}
|
||||
} else {
|
||||
val file = java.io.File(path)
|
||||
if (file.exists()) {
|
||||
decodeSampledBitmapFile(path)
|
||||
decodeSampledBitmapFile(path, sampleBitmap)
|
||||
} else {
|
||||
val assetPath = if (path.startsWith("bg/")) path else "bg/$path"
|
||||
runCatching {
|
||||
ctx.assets.open(assetPath).use { input ->
|
||||
decodeSampledBitmap(input)
|
||||
decodeSampledBitmap(input, sampleBitmap)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -692,25 +814,40 @@ data class TextLine(
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeSampledBitmap(input: java.io.InputStream): Bitmap? {
|
||||
private fun decodeSampledBitmap(
|
||||
input: java.io.InputStream,
|
||||
sampleBitmap: Boolean,
|
||||
): Bitmap? {
|
||||
val buffered = if (input.markSupported()) input else java.io.BufferedInputStream(input)
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
buffered.mark(buffered.available())
|
||||
BitmapFactory.decodeStream(buffered, null, options)
|
||||
options.inSampleSize = calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
||||
options.inSampleSize = if (sampleBitmap) {
|
||||
calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
options.inJustDecodeBounds = false
|
||||
buffered.reset()
|
||||
return BitmapFactory.decodeStream(buffered, null, options)
|
||||
}
|
||||
|
||||
private fun decodeSampledBitmapFile(path: String): Bitmap? {
|
||||
private fun decodeSampledBitmapFile(path: String, sampleBitmap: Boolean): Bitmap? {
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(path, options)
|
||||
options.inSampleSize = calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
||||
options.inSampleSize = if (sampleBitmap) {
|
||||
calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
options.inJustDecodeBounds = false
|
||||
return BitmapFactory.decodeFile(path, options)
|
||||
}
|
||||
|
||||
private fun isRawNinePatchPath(path: String): Boolean {
|
||||
return path.substringBefore('?').substringBefore('#').endsWith(".9.png", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun calculateInSampleSize(
|
||||
options: BitmapFactory.Options,
|
||||
reqWidth: Int,
|
||||
|
||||
@@ -2,7 +2,9 @@ package io.legado.app.ui.book.read.sheet
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -135,14 +137,34 @@ fun HighlightRuleEditSheet(
|
||||
// Validation
|
||||
var patternError by remember(show, rule) { mutableStateOf<String?>(null) }
|
||||
|
||||
// SAF image picker
|
||||
// System photo picker, with the contract's built-in fallback on older devices.
|
||||
val imagePicker = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
ActivityResultContracts.PickVisualMedia()
|
||||
) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val dir = File(appCtx.filesDir, "bg_images")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val target = File(dir, "bg_${System.currentTimeMillis()}.jpg")
|
||||
val displayName = context.contentResolver.query(
|
||||
uri,
|
||||
arrayOf(OpenableColumns.DISPLAY_NAME),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
.takeIf { it >= 0 }
|
||||
?.let(cursor::getString)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val suffix = when {
|
||||
displayName?.endsWith(".9.png", ignoreCase = true) == true -> ".9.png"
|
||||
displayName?.substringAfterLast('.', "").isNullOrBlank() -> ".img"
|
||||
else -> ".${displayName.substringAfterLast('.')}"
|
||||
}
|
||||
val target = File(dir, "bg_${System.currentTimeMillis()}$suffix")
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
target.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
@@ -362,7 +384,11 @@ fun HighlightRuleEditSheet(
|
||||
TinyClickableSettingItem(
|
||||
title = stringResource(R.string.highlight_bg_image),
|
||||
description = bgImage.ifBlank { null }?.let { File(it).name },
|
||||
onClick = { imagePicker.launch("image/*") },
|
||||
onClick = {
|
||||
imagePicker.launch(
|
||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (hasBgImage && bgImage.isNotBlank()) {
|
||||
|
||||
Reference in New Issue
Block a user