fix: 使用图片选择器并支持九宫格高亮背景
This commit is contained in:
@@ -6,6 +6,7 @@ import android.view.View
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -165,14 +166,14 @@ fun ReadBookRouteScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val readStyleImagePicker = rememberLauncherForActivityResult(
|
val readStyleImagePicker = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.OpenDocument()
|
ActivityResultContracts.PickVisualMedia()
|
||||||
) { uri ->
|
) { uri ->
|
||||||
uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleImageSelected(it)) }
|
uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleImageSelected(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var pendingReadStyleImageIsNight by remember { mutableStateOf(false) }
|
var pendingReadStyleImageIsNight by remember { mutableStateOf(false) }
|
||||||
val readStyleImagePickerForMode = rememberLauncherForActivityResult(
|
val readStyleImagePickerForMode = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.OpenDocument()
|
ActivityResultContracts.PickVisualMedia()
|
||||||
) { uri ->
|
) { uri ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
viewModel.onIntent(ReadBookIntent.ReadStyleImageSelectedForMode(it, pendingReadStyleImageIsNight))
|
viewModel.onIntent(ReadBookIntent.ReadStyleImageSelectedForMode(it, pendingReadStyleImageIsNight))
|
||||||
@@ -367,11 +368,15 @@ fun ReadBookRouteScreen(
|
|||||||
booksDirPicker.launch(null)
|
booksDirPicker.launch(null)
|
||||||
}
|
}
|
||||||
is ReadBookEffect.OpenReadStyleImagePicker -> {
|
is ReadBookEffect.OpenReadStyleImagePicker -> {
|
||||||
readStyleImagePicker.launch(arrayOf("image/*"))
|
readStyleImagePicker.launch(
|
||||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is ReadBookEffect.OpenReadStyleImagePickerForMode -> {
|
is ReadBookEffect.OpenReadStyleImagePickerForMode -> {
|
||||||
pendingReadStyleImageIsNight = effect.isNight
|
pendingReadStyleImageIsNight = effect.isNight
|
||||||
readStyleImagePickerForMode.launch(arrayOf("image/*"))
|
readStyleImagePickerForMode.launch(
|
||||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is ReadBookEffect.OpenReadStyleImport -> {
|
is ReadBookEffect.OpenReadStyleImport -> {
|
||||||
readStyleImportPicker.launch(
|
readStyleImportPicker.launch(
|
||||||
|
|||||||
@@ -527,9 +527,12 @@ data class TextLine(
|
|||||||
val sh = rectHeight * scale
|
val sh = rectHeight * scale
|
||||||
val dx = startX + (rectWidth - sw) / 2f
|
val dx = startX + (rectWidth - sw) / 2f
|
||||||
val dy = top + (rectHeight - sh) / 2f
|
val dy = top + (rectHeight - sh) / 2f
|
||||||
|
val destination = android.graphics.RectF(dx, dy, dx + sw, dy + sh)
|
||||||
canvas.save()
|
canvas.save()
|
||||||
canvas.clipRect(startX, top, endX, bottom)
|
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()
|
canvas.restore()
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
@@ -565,6 +568,124 @@ data class TextLine(
|
|||||||
PaintPool.recycle(paint)
|
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 {
|
fun checkFastDraw(): Boolean {
|
||||||
if (!ReadConfig.optimizeRender || exceed || !onlyTextColumn || textPage.isMsgPage) {
|
if (!ReadConfig.optimizeRender || exceed || !onlyTextColumn || textPage.isMsgPage) {
|
||||||
return false
|
return false
|
||||||
@@ -664,25 +785,26 @@ data class TextLine(
|
|||||||
private fun loadBgBitmap(path: String): Bitmap? {
|
private fun loadBgBitmap(path: String): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
val ctx = appCtx
|
val ctx = appCtx
|
||||||
|
val sampleBitmap = !isRawNinePatchPath(path)
|
||||||
if (path.startsWith("assets://")) {
|
if (path.startsWith("assets://")) {
|
||||||
val assetPath = path.removePrefix("assets://")
|
val assetPath = path.removePrefix("assets://")
|
||||||
ctx.assets.open(assetPath).use { input ->
|
ctx.assets.open(assetPath).use { input ->
|
||||||
decodeSampledBitmap(input)
|
decodeSampledBitmap(input, sampleBitmap)
|
||||||
}
|
}
|
||||||
} else if (path.startsWith("content://")) {
|
} else if (path.startsWith("content://")) {
|
||||||
val uri = android.net.Uri.parse(path)
|
val uri = android.net.Uri.parse(path)
|
||||||
ctx.contentResolver.openInputStream(uri)?.use { input ->
|
ctx.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
decodeSampledBitmap(input)
|
decodeSampledBitmap(input, sampleBitmap)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val file = java.io.File(path)
|
val file = java.io.File(path)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
decodeSampledBitmapFile(path)
|
decodeSampledBitmapFile(path, sampleBitmap)
|
||||||
} else {
|
} else {
|
||||||
val assetPath = if (path.startsWith("bg/")) path else "bg/$path"
|
val assetPath = if (path.startsWith("bg/")) path else "bg/$path"
|
||||||
runCatching {
|
runCatching {
|
||||||
ctx.assets.open(assetPath).use { input ->
|
ctx.assets.open(assetPath).use { input ->
|
||||||
decodeSampledBitmap(input)
|
decodeSampledBitmap(input, sampleBitmap)
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.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 buffered = if (input.markSupported()) input else java.io.BufferedInputStream(input)
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
buffered.mark(buffered.available())
|
buffered.mark(buffered.available())
|
||||||
BitmapFactory.decodeStream(buffered, null, options)
|
BitmapFactory.decodeStream(buffered, null, options)
|
||||||
options.inSampleSize = calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
options.inSampleSize = if (sampleBitmap) {
|
||||||
|
calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
options.inJustDecodeBounds = false
|
options.inJustDecodeBounds = false
|
||||||
buffered.reset()
|
buffered.reset()
|
||||||
return BitmapFactory.decodeStream(buffered, null, options)
|
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 }
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
BitmapFactory.decodeFile(path, options)
|
BitmapFactory.decodeFile(path, options)
|
||||||
options.inSampleSize = calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
options.inSampleSize = if (sampleBitmap) {
|
||||||
|
calculateInSampleSize(options, bgSampleWidth, bgSampleHeight)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
options.inJustDecodeBounds = false
|
options.inJustDecodeBounds = false
|
||||||
return BitmapFactory.decodeFile(path, options)
|
return BitmapFactory.decodeFile(path, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isRawNinePatchPath(path: String): Boolean {
|
||||||
|
return path.substringBefore('?').substringBefore('#').endsWith(".9.png", ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
private fun calculateInSampleSize(
|
private fun calculateInSampleSize(
|
||||||
options: BitmapFactory.Options,
|
options: BitmapFactory.Options,
|
||||||
reqWidth: Int,
|
reqWidth: Int,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package io.legado.app.ui.book.read.sheet
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -135,14 +137,34 @@ fun HighlightRuleEditSheet(
|
|||||||
// Validation
|
// Validation
|
||||||
var patternError by remember(show, rule) { mutableStateOf<String?>(null) }
|
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(
|
val imagePicker = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.GetContent()
|
ActivityResultContracts.PickVisualMedia()
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
val dir = File(appCtx.filesDir, "bg_images")
|
val dir = File(appCtx.filesDir, "bg_images")
|
||||||
if (!dir.exists()) dir.mkdirs()
|
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 ->
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
target.outputStream().use { output ->
|
target.outputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
@@ -362,7 +384,11 @@ fun HighlightRuleEditSheet(
|
|||||||
TinyClickableSettingItem(
|
TinyClickableSettingItem(
|
||||||
title = stringResource(R.string.highlight_bg_image),
|
title = stringResource(R.string.highlight_bg_image),
|
||||||
description = bgImage.ifBlank { null }?.let { File(it).name },
|
description = bgImage.ifBlank { null }?.let { File(it).name },
|
||||||
onClick = { imagePicker.launch("image/*") },
|
onClick = {
|
||||||
|
imagePicker.launch(
|
||||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (hasBgImage && bgImage.isNotBlank()) {
|
if (hasBgImage && bgImage.isNotBlank()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user