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