diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index c566d6aab..16e5dce9d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -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() + isShowingSearchResult -> binding.searchMenu.runMenuIn() else -> binding.readMenu.runMenuIn() } } @@ -766,6 +769,14 @@ class ReadBookActivity : BaseReadBookActivity(), showDialogFragment() } + 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 { diff --git a/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt new file mode 100644 index 000000000..8341062ce --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt @@ -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() + + 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 { + 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() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt index 5bb46882e..dc074a29a 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt @@ -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 = 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(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() 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 { - val searchResults: MutableList = mutableListOf() - var positions: List - var replaceContents: List? - 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 { - val position: MutableList = 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 { - // 构建关键词周边文字,在搜索结果里显示 - // 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() } diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt index 364e7daaf..426f50864 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt @@ -14,7 +14,6 @@ import io.legado.app.utils.hexString class SearchContentAdapter(context: Context, val callback: Callback) : RecyclerAdapter(context) { - val cacheFileNames = hashSetOf() val textColor = context.getCompatColor(R.color.primaryText).hexString.substring(2) val accentColor = context.accentColor.hexString.substring(2) diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt index 121248738..847e301a0 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt @@ -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() + val searchResultList: MutableList = 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 { + val searchResultsWithinChapter: MutableList = 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 { + val position: MutableList = 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 { + // 左右移动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 + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt index 3dc4e9fc3..129fd6e05 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt @@ -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) + - "($chapterTitle)" + 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 "$sub1" + - "$center" + - "$sub2" - } + private fun String.colorTextForHtml(textColor: String) = "$this" } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_read.xml b/app/src/main/res/layout/activity_book_read.xml index 235c04b87..703c0ef5e 100644 --- a/app/src/main/res/layout/activity_book_read.xml +++ b/app/src/main/res/layout/activity_book_read.xml @@ -37,6 +37,12 @@ android:layout_height="match_parent" android:visibility="gone" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file