mirror of
https://github.com/gedoor/legado.git
synced 2024-09-01 09:34:25 +08:00
commit
20a516196a
@ -65,6 +65,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
TextActionMenu.CallBack,
|
||||
ContentTextView.CallBack,
|
||||
ReadMenu.CallBack,
|
||||
SearchMenu.CallBack,
|
||||
ReadAloudDialog.CallBack,
|
||||
ChangeSourceDialog.CallBack,
|
||||
ReadBook.CallBack,
|
||||
@ -117,6 +118,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
private var backupJob: Job? = null
|
||||
override var autoPageProgress = 0
|
||||
override var isAutoPage = false
|
||||
override var isShowingSearchResult = true
|
||||
private var screenTimeOut: Long = 0
|
||||
private var timeBatteryReceiver: TimeBatteryReceiver? = null
|
||||
private var loadStates: Boolean = false
|
||||
@ -635,6 +637,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
when {
|
||||
BaseReadAloudService.isRun -> showReadAloudDialog()
|
||||
isAutoPage -> showDialogFragment<AutoReadDialog>()
|
||||
isShowingSearchResult -> binding.searchMenu.runMenuIn()
|
||||
else -> binding.readMenu.runMenuIn()
|
||||
}
|
||||
}
|
||||
@ -766,6 +769,14 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
showDialogFragment<MoreConfigDialog>()
|
||||
}
|
||||
|
||||
override fun returnSearchActivity() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun showSearchSetting() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态栏,导航栏
|
||||
*/
|
||||
@ -774,6 +785,13 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
upNavigationBarColor()
|
||||
}
|
||||
|
||||
override fun searchExit() {
|
||||
if(isShowingSearchResult){
|
||||
isShowingSearchResult = false
|
||||
binding.searchMenu.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showLogin() {
|
||||
ReadBook.bookSource?.let {
|
||||
startActivity<SourceLoginActivity> {
|
||||
|
204
app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt
Normal file
204
app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt
Normal file
@ -0,0 +1,204 @@
|
||||
package io.legado.app.ui.book.read
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.Animation
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.databinding.ViewSearchMenuBinding
|
||||
import io.legado.app.help.*
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.*
|
||||
import io.legado.app.model.ReadBook
|
||||
import io.legado.app.ui.book.info.BookInfoActivity
|
||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||
import io.legado.app.ui.browser.WebViewActivity
|
||||
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
||||
import io.legado.app.utils.*
|
||||
import splitties.views.*
|
||||
|
||||
/**
|
||||
* 阅读界面菜单
|
||||
*/
|
||||
class SearchMenu @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
private val searchView: SearchView by lazy {
|
||||
binding.titleBar.findViewById(R.id.search_view)
|
||||
}
|
||||
|
||||
val viewModel by viewModels<SearchContentViewModel>()
|
||||
|
||||
private val callBack: CallBack get() = activity as CallBack
|
||||
private val binding = ViewSearchMenuBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
private val menuTopIn: Animation = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_in)
|
||||
private val menuTopOut: Animation = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_out)
|
||||
private val menuBottomIn: Animation = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in)
|
||||
private val menuBottomOut: Animation = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out)
|
||||
private val bgColor: Int = context.bottomBackground
|
||||
private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
|
||||
private val bottomBackgroundList: ColorStateList =
|
||||
Selector.colorBuild().setDefaultColor(bgColor).setPressedColor(ColorUtils.darkenColor(bgColor)).create()
|
||||
private var onMenuOutEnd: (() -> Unit)? = null
|
||||
private var hasSearchResult: Boolean = true
|
||||
|
||||
init {
|
||||
initAnimation()
|
||||
initView()
|
||||
bindEvent()
|
||||
}
|
||||
|
||||
private fun initView() = binding.run {
|
||||
llSearchBaseInfo.setBackgroundColor(bgColor)
|
||||
tvCurrentSearchInfo.setTextColor(bottomBackgroundList)
|
||||
llBottomBg.setBackgroundColor(bgColor)
|
||||
fabLeft.backgroundTintList = bottomBackgroundList
|
||||
fabLeft.setColorFilter(textColor)
|
||||
fabRight.backgroundTintList = bottomBackgroundList
|
||||
fabRight.setColorFilter(textColor)
|
||||
tvMainMenu.setTextColor(textColor)
|
||||
tvSearchResults.setTextColor(textColor)
|
||||
tvSearchExit.setTextColor(textColor)
|
||||
tvSetting.setTextColor(textColor)
|
||||
ivMainMenu.setColorFilter(textColor)
|
||||
ivSearchResults.setColorFilter(textColor)
|
||||
ivSearchExit.setColorFilter(textColor)
|
||||
ivSetting.setColorFilter(textColor)
|
||||
ivSearchContentBottom.setColorFilter(textColor)
|
||||
ivSearchContentTop.setColorFilter(textColor)
|
||||
}
|
||||
|
||||
|
||||
fun runMenuIn() {
|
||||
this.visible()
|
||||
binding.titleBar.visible()
|
||||
binding.llSearchBaseInfo.visible()
|
||||
binding.llBottomBg.visible()
|
||||
binding.titleBar.startAnimation(menuTopIn)
|
||||
binding.llSearchBaseInfo.startAnimation(menuBottomIn)
|
||||
binding.llBottomBg.startAnimation(menuBottomIn)
|
||||
}
|
||||
|
||||
fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) {
|
||||
this.onMenuOutEnd = onMenuOutEnd
|
||||
if (this.isVisible) {
|
||||
binding.titleBar.startAnimation(menuTopOut)
|
||||
binding.llSearchBaseInfo.startAnimation(menuBottomOut)
|
||||
binding.llBottomBg.startAnimation(menuBottomOut)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindEvent() = binding.run {
|
||||
titleBar.toolbar.setOnClickListener {
|
||||
ReadBook.book?.let {
|
||||
context.startActivity<BookInfoActivity> {
|
||||
putExtra("name", it.name)
|
||||
putExtra("author", it.author)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
llSearchResults.setOnClickListener {
|
||||
runMenuOut {
|
||||
callBack.returnSearchActivity()
|
||||
}
|
||||
}
|
||||
|
||||
//主菜单
|
||||
llMainMenu.setOnClickListener {
|
||||
runMenuOut {
|
||||
callBack.showMenuBar()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//目录
|
||||
llSearchExit.setOnClickListener {
|
||||
runMenuOut {
|
||||
callBack.searchExit()
|
||||
}
|
||||
}
|
||||
|
||||
//设置
|
||||
llSetting.setOnClickListener {
|
||||
runMenuOut {
|
||||
callBack.showSearchSetting()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAnimation() {
|
||||
//显示菜单
|
||||
menuTopIn.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
callBack.upSystemUiVisibility()
|
||||
binding.fabLeft.visible(hasSearchResult)
|
||||
binding.fabRight.visible(hasSearchResult)
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
val navigationBarHeight = if (ReadBookConfig.hideNavigationBar) {
|
||||
activity?.navigationBarHeight ?: 0
|
||||
} else {
|
||||
0
|
||||
}
|
||||
binding.run {
|
||||
vwMenuBg.setOnClickListener { runMenuOut() }
|
||||
root.padding = 0
|
||||
when (activity?.navigationBarGravity) {
|
||||
Gravity.BOTTOM -> root.bottomPadding = navigationBarHeight
|
||||
Gravity.LEFT -> root.leftPadding = navigationBarHeight
|
||||
Gravity.RIGHT -> root.rightPadding = navigationBarHeight
|
||||
}
|
||||
}
|
||||
callBack.upSystemUiVisibility()
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
||||
})
|
||||
|
||||
//隐藏菜单
|
||||
menuTopOut.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
binding.vwMenuBg.setOnClickListener(null)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
this@SearchMenu.invisible()
|
||||
binding.titleBar.invisible()
|
||||
binding.llSearchBaseInfo.invisible()
|
||||
binding.llBottomBg.invisible()
|
||||
binding.fabRight.invisible()
|
||||
binding.fabLeft.invisible()
|
||||
onMenuOutEnd?.invoke()
|
||||
callBack.upSystemUiVisibility()
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
interface CallBack {
|
||||
var isShowingSearchResult: Boolean
|
||||
fun returnSearchActivity()
|
||||
fun showSearchSetting()
|
||||
fun upSystemUiVisibility()
|
||||
fun searchExit()
|
||||
fun showMenuBar()
|
||||
}
|
||||
|
||||
}
|
@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.EventBus
|
||||
@ -13,7 +12,6 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.databinding.ActivitySearchContentBinding
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.lib.theme.bottomBackground
|
||||
import io.legado.app.lib.theme.getPrimaryTextColor
|
||||
@ -40,9 +38,7 @@ class SearchContentActivity :
|
||||
private val searchView: SearchView by lazy {
|
||||
binding.titleBar.findViewById(R.id.search_view)
|
||||
}
|
||||
private var searchResultCounts = 0
|
||||
private var durChapterIndex = 0
|
||||
private var searchResultList: MutableList<SearchResult> = mutableListOf()
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
val bbg = bottomBackground
|
||||
@ -103,7 +99,7 @@ class SearchContentActivity :
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun initBook() {
|
||||
binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts"
|
||||
binding.tvCurrentSearchInfo.text = "搜索结果:${viewModel.searchResultCounts}"
|
||||
viewModel.book?.let {
|
||||
initCacheFileNames(it)
|
||||
durChapterIndex = it.durChapterIndex
|
||||
@ -115,7 +111,7 @@ class SearchContentActivity :
|
||||
|
||||
private fun initCacheFileNames(book: Book) {
|
||||
launch(Dispatchers.IO) {
|
||||
adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book))
|
||||
viewModel.cacheChapterNames.addAll(BookHelp.getChapterFiles(book))
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount, true)
|
||||
}
|
||||
@ -126,7 +122,7 @@ class SearchContentActivity :
|
||||
observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter ->
|
||||
viewModel.book?.bookUrl?.let { bookUrl ->
|
||||
if (chapter.bookUrl == bookUrl) {
|
||||
adapter.cacheFileNames.add(chapter.getFileName())
|
||||
viewModel.cacheChapterNames.add(chapter.getFileName())
|
||||
adapter.notifyItemChanged(chapter.index, true)
|
||||
}
|
||||
}
|
||||
@ -134,28 +130,26 @@ class SearchContentActivity :
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun startContentSearch(newText: String) {
|
||||
fun startContentSearch(query: String) {
|
||||
// 按章节搜索内容
|
||||
if (newText.isNotBlank()) {
|
||||
if (query.isNotBlank()) {
|
||||
adapter.clearItems()
|
||||
searchResultList.clear()
|
||||
binding.refreshProgressBar.isAutoLoading = true
|
||||
searchResultCounts = 0
|
||||
viewModel.lastQuery = newText
|
||||
viewModel.searchResultList.clear()
|
||||
viewModel.searchResultCounts = 0
|
||||
viewModel.lastQuery = query
|
||||
var searchResults = listOf<SearchResult>()
|
||||
launch(Dispatchers.Main) {
|
||||
appDb.bookChapterDao.getChapterList(viewModel.bookUrl).map { chapter ->
|
||||
appDb.bookChapterDao.getChapterList(viewModel.bookUrl).map { bookChapter ->
|
||||
binding.refreshProgressBar.isAutoLoading = true
|
||||
withContext(Dispatchers.IO) {
|
||||
if (isLocalBook
|
||||
|| adapter.cacheFileNames.contains(chapter.getFileName())
|
||||
) {
|
||||
searchResults = searchChapter(newText, chapter)
|
||||
if (isLocalBook || viewModel.cacheChapterNames.contains(bookChapter.getFileName())) {
|
||||
searchResults = viewModel.searchChapter(query, bookChapter)
|
||||
}
|
||||
}
|
||||
if (searchResults.isNotEmpty()) {
|
||||
searchResultList.addAll(searchResults)
|
||||
viewModel.searchResultList.addAll(searchResults)
|
||||
binding.refreshProgressBar.isAutoLoading = false
|
||||
binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts"
|
||||
binding.tvCurrentSearchInfo.text = "搜索结果:${viewModel.searchResultCounts}"
|
||||
adapter.addItems(searchResults)
|
||||
searchResults = listOf()
|
||||
}
|
||||
@ -164,85 +158,6 @@ class SearchContentActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
|
||||
val searchResults: MutableList<SearchResult> = mutableListOf()
|
||||
var positions: List<Int>
|
||||
var replaceContents: List<String>?
|
||||
var totalContents: String
|
||||
if (chapter != null) {
|
||||
viewModel.book?.let { book ->
|
||||
val bookContent = BookHelp.getContent(book, chapter)
|
||||
if (bookContent != null) {
|
||||
//搜索替换后的正文
|
||||
withContext(Dispatchers.IO) {
|
||||
chapter.title = when (AppConfig.chineseConverterType) {
|
||||
1 -> ChineseUtils.t2s(chapter.title)
|
||||
2 -> ChineseUtils.s2t(chapter.title)
|
||||
else -> chapter.title
|
||||
}
|
||||
replaceContents =
|
||||
viewModel.contentProcessor!!.getContent(
|
||||
book,
|
||||
chapter,
|
||||
bookContent,
|
||||
chineseConvert = false,
|
||||
reSegment = false
|
||||
)
|
||||
}
|
||||
totalContents = replaceContents?.joinToString("") ?: bookContent
|
||||
positions = searchPosition(totalContents, query)
|
||||
var count = 1
|
||||
positions.map {
|
||||
val construct = constructText(totalContents, it, query)
|
||||
val result = SearchResult(
|
||||
index = searchResultCounts,
|
||||
indexWithinChapter = count,
|
||||
text = construct[1] as String,
|
||||
chapterTitle = chapter.title,
|
||||
query = query,
|
||||
chapterIndex = chapter.index,
|
||||
newPosition = construct[0] as Int,
|
||||
contentPosition = it
|
||||
)
|
||||
count += 1
|
||||
searchResultCounts += 1
|
||||
searchResults.add(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return searchResults
|
||||
}
|
||||
|
||||
private fun searchPosition(content: String, pattern: String): List<Int> {
|
||||
val position: MutableList<Int> = mutableListOf()
|
||||
var index = content.indexOf(pattern)
|
||||
while (index >= 0) {
|
||||
position.add(index)
|
||||
index = content.indexOf(pattern, index + 1)
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
private fun constructText(content: String, position: Int, query: String): Array<Any> {
|
||||
// 构建关键词周边文字,在搜索结果里显示
|
||||
// todo: 判断段落,只在关键词所在段落内分割
|
||||
// todo: 利用标点符号分割完整的句
|
||||
// todo: length和设置结合,自由调整周边文字长度
|
||||
val length = 20
|
||||
var po1 = position - length
|
||||
var po2 = position + query.length + length
|
||||
if (po1 < 0) {
|
||||
po1 = 0
|
||||
}
|
||||
if (po2 > content.length) {
|
||||
po2 = content.length
|
||||
}
|
||||
val newPosition = position - po1
|
||||
val newText = content.substring(po1, po2)
|
||||
return arrayOf(newPosition, newText)
|
||||
}
|
||||
|
||||
val isLocalBook: Boolean
|
||||
get() = viewModel.book?.isLocalBook() == true
|
||||
|
||||
@ -251,7 +166,7 @@ class SearchContentActivity :
|
||||
searchData.putExtra("index", searchResult.chapterIndex)
|
||||
searchData.putExtra("contentPosition", searchResult.contentPosition)
|
||||
searchData.putExtra("query", searchResult.query)
|
||||
searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter)
|
||||
searchData.putExtra("indexWithinChapter", searchResult.resultCountWithinChapter)
|
||||
setResult(RESULT_OK, searchData)
|
||||
finish()
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import io.legado.app.utils.hexString
|
||||
class SearchContentAdapter(context: Context, val callback: Callback) :
|
||||
RecyclerAdapter<SearchResult, ItemSearchListBinding>(context) {
|
||||
|
||||
val cacheFileNames = hashSetOf<String>()
|
||||
val textColor = context.getCompatColor(R.color.primaryText).hexString.substring(2)
|
||||
val accentColor = context.accentColor.hexString.substring(2)
|
||||
|
||||
|
@ -2,16 +2,25 @@ package io.legado.app.ui.book.searchContent
|
||||
|
||||
|
||||
import android.app.Application
|
||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SearchContentViewModel(application: Application) : BaseViewModel(application) {
|
||||
var bookUrl: String = ""
|
||||
var book: Book? = null
|
||||
var contentProcessor: ContentProcessor? = null
|
||||
private var contentProcessor: ContentProcessor? = null
|
||||
var lastQuery: String = ""
|
||||
var searchResultCounts = 0
|
||||
val cacheChapterNames = hashSetOf<String>()
|
||||
val searchResultList: MutableList<SearchResult> = mutableListOf()
|
||||
|
||||
fun initBook(bookUrl: String, success: () -> Unit) {
|
||||
this.bookUrl = bookUrl
|
||||
@ -25,4 +34,72 @@ class SearchContentViewModel(application: Application) : BaseViewModel(applicati
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
|
||||
val searchResultsWithinChapter: MutableList<SearchResult> = mutableListOf()
|
||||
if (chapter != null) {
|
||||
book?.let { book ->
|
||||
val chapterContent = BookHelp.getContent(book, chapter)
|
||||
if (chapterContent != null) {
|
||||
//搜索替换后的正文
|
||||
val replaceContent: String
|
||||
withContext(Dispatchers.IO) {
|
||||
chapter.title = when (AppConfig.chineseConverterType) {
|
||||
1 -> ChineseUtils.t2s(chapter.title)
|
||||
2 -> ChineseUtils.s2t(chapter.title)
|
||||
else -> chapter.title
|
||||
}
|
||||
replaceContent = contentProcessor!!.getContent(
|
||||
book, chapter, chapterContent, chineseConvert = false, reSegment = false
|
||||
).joinToString("")
|
||||
}
|
||||
val positions = searchPosition(replaceContent, query)
|
||||
positions.forEachIndexed { index, position ->
|
||||
val construct = getResultAndQueryIndex(replaceContent, position, query)
|
||||
val result = SearchResult(
|
||||
resultCountWithinChapter = index,
|
||||
resultText = construct.second,
|
||||
chapterTitle = chapter.title,
|
||||
query = query,
|
||||
chapterIndex = chapter.index,
|
||||
queryIndexInResult = construct.first,
|
||||
contentPosition = position
|
||||
)
|
||||
searchResultsWithinChapter.add(result)
|
||||
}
|
||||
searchResultCounts += searchResultsWithinChapter.size
|
||||
}
|
||||
}
|
||||
}
|
||||
return searchResultsWithinChapter
|
||||
}
|
||||
|
||||
private fun searchPosition(content: String, pattern: String): List<Int> {
|
||||
val position: MutableList<Int> = mutableListOf()
|
||||
var index = content.indexOf(pattern)
|
||||
while (index >= 0) {
|
||||
position.add(index)
|
||||
index = content.indexOf(pattern, index + 1)
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
private fun getResultAndQueryIndex(content: String, queryIndexInContent: Int, query: String): Pair<Int, String> {
|
||||
// 左右移动20个字符,构建关键词周边文字,在搜索结果里显示
|
||||
// todo: 判断段落,只在关键词所在段落内分割
|
||||
// todo: 利用标点符号分割完整的句
|
||||
// todo: length和设置结合,自由调整周边文字长度
|
||||
val length = 20
|
||||
var po1 = queryIndexInContent - length
|
||||
var po2 = queryIndexInContent + query.length + length
|
||||
if (po1 < 0) {
|
||||
po1 = 0
|
||||
}
|
||||
if (po2 > content.length) {
|
||||
po2 = content.length
|
||||
}
|
||||
val queryIndexInResult = queryIndexInContent - po1
|
||||
val newText = content.substring(po1, po2)
|
||||
return queryIndexInResult to newText
|
||||
}
|
||||
|
||||
}
|
@ -4,36 +4,29 @@ import android.text.Spanned
|
||||
import androidx.core.text.HtmlCompat
|
||||
|
||||
data class SearchResult(
|
||||
var index: Int = 0,
|
||||
var indexWithinChapter: Int = 0,
|
||||
var text: String = "",
|
||||
var chapterTitle: String = "",
|
||||
val resultCount: Int = 0,
|
||||
val resultCountWithinChapter: Int = 0,
|
||||
val resultText: String = "",
|
||||
val chapterTitle: String = "",
|
||||
val query: String,
|
||||
var pageSize: Int = 0,
|
||||
var chapterIndex: Int = 0,
|
||||
var pageIndex: Int = 0,
|
||||
var newPosition: Int = 0,
|
||||
var contentPosition: Int = 0
|
||||
val pageSize: Int = 0,
|
||||
val chapterIndex: Int = 0,
|
||||
val pageIndex: Int = 0,
|
||||
val queryIndexInResult: Int = 0,
|
||||
val contentPosition: Int = 0
|
||||
) {
|
||||
|
||||
fun getHtmlCompat(textColor: String, accentColor: String): Spanned {
|
||||
val html = colorPresentText(newPosition, query, text, textColor, accentColor) +
|
||||
"<font color=#${accentColor}>($chapterTitle)</font>"
|
||||
val queryIndexInSurrounding = resultText.indexOf(query)
|
||||
val leftString = resultText.substring(0, queryIndexInSurrounding)
|
||||
val rightString = resultText.substring(queryIndexInSurrounding + query.length, resultText.length)
|
||||
val html = leftString.colorTextForHtml(textColor) +
|
||||
query.colorTextForHtml(accentColor) +
|
||||
rightString.colorTextForHtml(textColor) +
|
||||
chapterTitle.colorTextForHtml(accentColor)
|
||||
return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
private fun colorPresentText(
|
||||
position: Int,
|
||||
center: String,
|
||||
targetText: String,
|
||||
textColor: String,
|
||||
accentColor: String
|
||||
): String {
|
||||
val sub1 = text.substring(0, position)
|
||||
val sub2 = text.substring(position + center.length, targetText.length)
|
||||
return "<font color=#${textColor}>$sub1</font>" +
|
||||
"<font color=#${accentColor}>$center</font>" +
|
||||
"<font color=#${textColor}>$sub2</font>"
|
||||
}
|
||||
private fun String.colorTextForHtml(textColor: String) = "<font color=#${textColor}>$this</font>"
|
||||
|
||||
}
|
@ -37,6 +37,12 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<io.legado.app.ui.book.read.SearchMenu
|
||||
android:id="@+id/search_menu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/navigation_bar"
|
||||
android:layout_width="match_parent"
|
||||
|
276
app/src/main/res/layout/view_search_menu.xml
Normal file
276
app/src/main/res/layout/view_search_menu.xml
Normal file
@ -0,0 +1,276 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/vw_menu_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/content"
|
||||
tools:layout_editor_absoluteX="0dp"
|
||||
tools:layout_editor_absoluteY="0dp" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabLeft"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="上个结果"
|
||||
android:src="@drawable/ic_search"
|
||||
android:tint="@color/primaryText"
|
||||
android:tooltipText="@string/search_content"
|
||||
app:backgroundTint="@color/background_menu"
|
||||
app:elevation="2dp"
|
||||
app:fabSize="mini"
|
||||
app:pressedTranslationZ="2dp"
|
||||
tools:ignore="UnusedAttribute"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabRight"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="下个结果"
|
||||
android:src="@drawable/ic_search"
|
||||
android:tint="@color/primaryText"
|
||||
android:tooltipText="@string/search_content"
|
||||
app:backgroundTint="@color/background_menu"
|
||||
app:elevation="2dp"
|
||||
app:fabSize="mini"
|
||||
app:pressedTranslationZ="2dp"
|
||||
tools:ignore="UnusedAttribute"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<io.legado.app.ui.widget.TitleBar
|
||||
android:id="@+id/title_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:contentLayout="@layout/view_search"
|
||||
app:contentInsetRight="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<io.legado.app.ui.widget.anima.RefreshProgressBar
|
||||
android:id="@+id/refresh_progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/title_bar"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_search_base_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="36dp"
|
||||
android:background="@color/background_menu"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/ll_bottom_bg">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_current_search_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:ellipsize="middle"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/primaryText"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/iv_search_content_top"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/go_to_top"
|
||||
android:src="@drawable/ic_arrow_drop_up"
|
||||
android:tooltipText="@string/go_to_top"
|
||||
app:tint="@color/primaryText"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/iv_search_content_bottom"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/go_to_bottom"
|
||||
android:src="@drawable/ic_arrow_drop_down"
|
||||
android:tooltipText="@string/go_to_bottom"
|
||||
app:tint="@color/primaryText"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_bottom_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<!--结果按钮-->
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_search_results"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="结果"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/iv_search_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="结果"
|
||||
android:src="@drawable/ic_toc"
|
||||
app:tint="@color/primaryText"
|
||||
tools:ignore="NestedWeights" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_search_results"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="3dp"
|
||||
android:text="结果"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/primaryText"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2" />
|
||||
<!--调节按钮-->
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_main_menu"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/read_aloud"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/iv_main_menu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/main_menu"
|
||||
android:src="@drawable/ic_menu"
|
||||
app:tint="@color/primaryText"
|
||||
tools:ignore="NestedWeights" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_main_menu"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="3dp"
|
||||
android:text="@string/main_menu"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/primaryText"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2" />
|
||||
<!--界面按钮-->
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_search_exit"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="退出"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/iv_search_exit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="退出"
|
||||
android:src="@drawable/ic_auto_page_stop"
|
||||
app:tint="@color/primaryText"
|
||||
tools:ignore="NestedWeights" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_search_exit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="3dp"
|
||||
android:text="退出"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/primaryText"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2" />
|
||||
<!--设置按钮-->
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_setting"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/setting"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/iv_setting"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/aloud_config"
|
||||
android:src="@drawable/ic_settings"
|
||||
app:tint="@color/primaryText"
|
||||
tools:ignore="NestedWeights" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_setting"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="3dp"
|
||||
android:text="@string/setting"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/primaryText"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue
Block a user