検索結果を辿る際に二分探索する
This commit is contained in:
parent
b10f77a71f
commit
e3c19b5361
|
@ -1,7 +1,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "org.jetbrains.kotlin.android"
|
id "org.jetbrains.kotlin.android"
|
||||||
// id "org.jetbrains.kotlin.kapt"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
@ -163,11 +163,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//kapt {
|
|
||||||
// useBuildCache = true
|
|
||||||
//}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// desugar_jdk_libs 2.0.0 は AGP 7.4.0-alpha10 以降を要求する
|
// desugar_jdk_libs 2.0.0 は AGP 7.4.0-alpha10 以降を要求する
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
|
|
|
@ -28,8 +28,6 @@
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
|
|
||||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
|
||||||
|
|
||||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||||
**[] $VALUES;
|
**[] $VALUES;
|
||||||
public *;
|
public *;
|
||||||
|
|
|
@ -72,6 +72,71 @@ class ActText : AppCompatActivity() {
|
||||||
|
|
||||||
private class SearchResultSpan(color: Int) : BackgroundColorSpan(color)
|
private class SearchResultSpan(color: Int) : BackgroundColorSpan(color)
|
||||||
|
|
||||||
|
private class SearchResult(
|
||||||
|
val items: List<IntRange> = emptyList(),
|
||||||
|
val hasMore: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
) {
|
||||||
|
val size = items.size
|
||||||
|
|
||||||
|
fun findNext(curPos: Int, allowEqual: Boolean = false): IntRange? {
|
||||||
|
var start = 0
|
||||||
|
var end = items.size
|
||||||
|
while (end > start) {
|
||||||
|
val mid = (end + start) shr 1
|
||||||
|
val item = items[mid]
|
||||||
|
if (curPos in item) return when {
|
||||||
|
allowEqual -> item
|
||||||
|
else -> items.elementAtOrNull(mid + 1)
|
||||||
|
}
|
||||||
|
items.elementAtOrNull(mid - 1)?.let { prev ->
|
||||||
|
if (curPos in prev.last + 1 until item.first) return item
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
curPos > item.first -> start = mid + 1
|
||||||
|
else -> end = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findPrev(curPos: Int, allowEqual: Boolean = false): IntRange? {
|
||||||
|
var start = 0
|
||||||
|
var end = items.size
|
||||||
|
while (end > start) {
|
||||||
|
val mid = (end + start) shr 1
|
||||||
|
val item = items[mid]
|
||||||
|
if (curPos in item) return when {
|
||||||
|
allowEqual -> item
|
||||||
|
else -> items.elementAtOrNull(mid - 1)
|
||||||
|
}
|
||||||
|
items.elementAtOrNull(mid + 1)?.let { next ->
|
||||||
|
if (curPos in item.last + 1 until next.first) return item
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
curPos > item.first -> start = mid + 1
|
||||||
|
else -> end = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun index(curPos: Int): Int? {
|
||||||
|
var start = 0
|
||||||
|
var end = items.size
|
||||||
|
while (end > start) {
|
||||||
|
val mid = (end + start) shr 1
|
||||||
|
val item = items[mid]
|
||||||
|
when {
|
||||||
|
curPos in item -> return mid
|
||||||
|
curPos > item.first -> start = mid + 1
|
||||||
|
else -> end = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val views by lazy {
|
private val views by lazy {
|
||||||
ActTextBinding.inflate(layoutInflater)
|
ActTextBinding.inflate(layoutInflater)
|
||||||
}
|
}
|
||||||
|
@ -80,9 +145,7 @@ class ActText : AppCompatActivity() {
|
||||||
|
|
||||||
private var account: SavedAccount? = null
|
private var account: SavedAccount? = null
|
||||||
|
|
||||||
private var searchResult: List<IntRange> = emptyList()
|
private var searchResult = SearchResult()
|
||||||
|
|
||||||
private var searchError: String? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -115,7 +178,7 @@ class ActText : AppCompatActivity() {
|
||||||
?.let { daoSavedAccount.loadAccount(it) }
|
?.let { daoSavedAccount.loadAccount(it) }
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
searchHighlight(null)
|
showSearchResult(null)
|
||||||
|
|
||||||
val sv = intent.string(EXTRA_TEXT) ?: ""
|
val sv = intent.string(EXTRA_TEXT) ?: ""
|
||||||
val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0
|
val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0
|
||||||
|
@ -277,22 +340,31 @@ class ActText : AppCompatActivity() {
|
||||||
val keyword = views.etSearch.text?.toString() ?: ""
|
val keyword = views.etSearch.text?.toString() ?: ""
|
||||||
val content = views.etText.text?.toString() ?: ""
|
val content = views.etText.text?.toString() ?: ""
|
||||||
val useRegex = views.btnToggleRegex.isChecked
|
val useRegex = views.btnToggleRegex.isChecked
|
||||||
val searchResult: List<IntRange> = withContext(AppDispatchers.IO) {
|
this.searchResult = withContext(AppDispatchers.IO) {
|
||||||
buildList {
|
|
||||||
searchError = null
|
|
||||||
if (keyword.isEmpty()) {
|
|
||||||
// nothing to do.
|
|
||||||
} else if (useRegex) {
|
|
||||||
try {
|
try {
|
||||||
|
val limit = 1000
|
||||||
|
var hasMore = false
|
||||||
|
val items = buildList {
|
||||||
|
when {
|
||||||
|
// 空欄
|
||||||
|
keyword.isEmpty() -> Unit
|
||||||
|
|
||||||
|
// 正規表現
|
||||||
|
useRegex -> {
|
||||||
val re = keyword.toRegex(RegexOption.IGNORE_CASE)
|
val re = keyword.toRegex(RegexOption.IGNORE_CASE)
|
||||||
re.findAll(content).forEach { mr ->
|
var nextStart = 0
|
||||||
|
while (nextStart < content.length) {
|
||||||
|
val mr = re.find(content, startIndex = nextStart) ?: break
|
||||||
|
if (size >= limit) {
|
||||||
|
hasMore = true
|
||||||
|
break
|
||||||
|
}
|
||||||
add(mr.range)
|
add(mr.range)
|
||||||
|
nextStart = mr.range.last + 1
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.e(ex, "search error.")
|
|
||||||
searchError = ex.message
|
|
||||||
}
|
}
|
||||||
} else {
|
// 生テキスト
|
||||||
|
else -> {
|
||||||
var nextStart = 0
|
var nextStart = 0
|
||||||
while (nextStart < content.length) {
|
while (nextStart < content.length) {
|
||||||
val pos = content.indexOf(
|
val pos = content.indexOf(
|
||||||
|
@ -301,6 +373,10 @@ class ActText : AppCompatActivity() {
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
)
|
)
|
||||||
if (pos == -1) break
|
if (pos == -1) break
|
||||||
|
if (size >= limit) {
|
||||||
|
hasMore = true
|
||||||
|
break
|
||||||
|
}
|
||||||
val end = pos + keyword.length
|
val end = pos + keyword.length
|
||||||
add(pos until end)
|
add(pos until end)
|
||||||
nextStart = end
|
nextStart = end
|
||||||
|
@ -308,44 +384,39 @@ class ActText : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.searchResult = searchResult
|
SearchResult(items = items, hasMore = hasMore)
|
||||||
when {
|
} catch (ex: Throwable) {
|
||||||
searchResult.isEmpty() -> searchHighlight(null)
|
log.e(ex, "search error.")
|
||||||
else -> searchNext(byTextUpdate = true)
|
SearchResult(error = ex.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showSearchResult {
|
||||||
|
searchResult.findNext(views.etText.selectionStart, allowEqual = true)
|
||||||
|
?: searchResult.items.firstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchNext(byTextUpdate: Boolean = false) {
|
private fun searchNext() {
|
||||||
try {
|
showSearchResult {
|
||||||
val curPos = views.etText.selectionStart
|
searchResult.findNext(views.etText.selectionStart, allowEqual = false)
|
||||||
val newPos = when {
|
?: searchResult.items.firstOrNull()
|
||||||
byTextUpdate -> searchResult.find { it.first >= curPos }
|
|
||||||
else -> searchResult.find { it.first > curPos }
|
|
||||||
} ?: searchResult.firstOrNull()
|
|
||||||
searchJump(newPos)
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.e(ex, "searchNext failed.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchPrev() {
|
private fun searchPrev() {
|
||||||
try {
|
showSearchResult {
|
||||||
val curPos = views.etText.selectionStart.takeIf { it >= 0 }
|
searchResult.findPrev(views.etText.selectionStart, allowEqual = false)
|
||||||
?: views.etText.text?.length
|
?: searchResult.items.lastOrNull()
|
||||||
?: return
|
}
|
||||||
val newPos = searchResult.findLast { it.first < curPos }
|
}
|
||||||
?: searchResult.lastOrNull()
|
|
||||||
searchJump(newPos)
|
private fun showSearchResult(newPosFinder: (() -> IntRange?)?) {
|
||||||
|
val newPos: IntRange? = try {
|
||||||
|
newPosFinder?.invoke()
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.e(ex, "searchPrev failed.")
|
log.e(ex, "newPosFinder failed.")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchJump(newPos: IntRange?) {
|
|
||||||
searchHighlight(newPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchHighlight(newPos: IntRange?) {
|
|
||||||
val hasKeyword = !views.etSearch.text.isNullOrEmpty()
|
val hasKeyword = !views.etSearch.text.isNullOrEmpty()
|
||||||
|
|
||||||
views.btnSearchClear.isEnabledAlpha = hasKeyword
|
views.btnSearchClear.isEnabledAlpha = hasKeyword
|
||||||
|
@ -357,21 +428,23 @@ class ActText : AppCompatActivity() {
|
||||||
val idx = newPos?.let {
|
val idx = newPos?.let {
|
||||||
val end = views.etText.text?.length ?: 0
|
val end = views.etText.text?.length ?: 0
|
||||||
views.etText.setSelection(
|
views.etText.setSelection(
|
||||||
newPos.first.clip(0, end),
|
it.first.clip(0, end),
|
||||||
(newPos.last + 1).clip(0, end),
|
(it.last + 1).clip(0, end),
|
||||||
)
|
)
|
||||||
searchResult.indexOf(newPos).takeIf { it >= 0 }
|
searchResult.index(it.first)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.tvSearchCount.text = getString(
|
views.tvSearchCount.text = getString(
|
||||||
R.string.search_result,
|
R.string.search_result,
|
||||||
idx?.plus(1) ?: 0,
|
idx?.plus(1) ?: 0,
|
||||||
searchResult.size
|
searchResult.size,
|
||||||
|
if (searchResult.hasMore) "+" else ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.tvSearchError.vg(hasKeyword && !searchError.isNullOrBlank())
|
val error = searchResult.error
|
||||||
?.text = searchError
|
views.tvSearchError.vg(hasKeyword && !error.isNullOrBlank())
|
||||||
|
?.text = error
|
||||||
|
|
||||||
views.etText.text?.let { e ->
|
views.etText.text?.let { e ->
|
||||||
for (span in e.getSpans(0, e.length, SearchResultSpan::class.java)) {
|
for (span in e.getSpans(0, e.length, SearchResultSpan::class.java)) {
|
||||||
|
@ -380,7 +453,7 @@ class ActText : AppCompatActivity() {
|
||||||
} catch (ignored: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (pos in searchResult) {
|
for (pos in searchResult.items) {
|
||||||
val attrId = when (newPos) {
|
val attrId = when (newPos) {
|
||||||
pos -> R.attr.colorSearchFormBackground
|
pos -> R.attr.colorSearchFormBackground
|
||||||
else -> R.attr.colorButtonBgCw
|
else -> R.attr.colorButtonBgCw
|
||||||
|
|
|
@ -1295,6 +1295,6 @@
|
||||||
<string name="save_to_local_folder">Save to local folder</string>
|
<string name="save_to_local_folder">Save to local folder</string>
|
||||||
<string name="app_data_export_import">Export/Import app data</string>
|
<string name="app_data_export_import">Export/Import app data</string>
|
||||||
<string name="translate_or_custom_share">Translate/Custom share buttons</string>
|
<string name="translate_or_custom_share">Translate/Custom share buttons</string>
|
||||||
<string name="search_result" translatable="false">%1$d/%2$d</string>
|
<string name="search_result" translatable="false">%1$d/%2$d%3$s</string>
|
||||||
<string name="toggle_regexp">.+\?</string>
|
<string name="toggle_regexp">.+\?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -26,7 +26,7 @@ buildscript {
|
||||||
ext.kotlinxCoroutinesVersion = "1.6.4"
|
ext.kotlinxCoroutinesVersion = "1.6.4"
|
||||||
ext.kspVersion = "1.8.20-1.0.11"
|
ext.kspVersion = "1.8.20-1.0.11"
|
||||||
ext.lifecycleVersion = "2.6.1"
|
ext.lifecycleVersion = "2.6.1"
|
||||||
ext.materialVersion = "1.8.0"
|
ext.materialVersion = "1.9.0"
|
||||||
ext.okhttpVersion = "4.10.0"
|
ext.okhttpVersion = "4.10.0"
|
||||||
ext.preferenceVersion = "1.2.0"
|
ext.preferenceVersion = "1.2.0"
|
||||||
ext.stBuildToolsVersion = "33.0.1"
|
ext.stBuildToolsVersion = "33.0.1"
|
||||||
|
|
Loading…
Reference in New Issue