Merge pull request #497 from gedoor/master

update
pull/500/head
Antecer 4 years ago committed by GitHub
commit 5ff58b7825
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/src/main/assets/help/appHelp.md
  2. 20
      app/src/main/assets/help/sourceHelp.md
  3. 9
      app/src/main/assets/updateLog.md
  4. 6
      app/src/main/java/io/legado/app/constant/AppConst.kt
  5. 4
      app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt
  6. 28
      app/src/main/java/io/legado/app/help/CacheManager.kt
  7. 46
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  8. 12
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  9. 33
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  10. 30
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  11. 18
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  12. 7
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  13. 11
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt
  14. 17
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  15. 17
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  16. 21
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  17. 11
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt
  18. 5
      app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt
  19. 11
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  20. 12
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  21. 437
      app/src/main/java/io/legado/app/ui/widget/text/BetterLinkMovementMethod.kt
  22. 1
      app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt
  23. 4
      app/src/main/res/values-zh-rHK/strings.xml
  24. 4
      app/src/main/res/values-zh-rTW/strings.xml
  25. 4
      app/src/main/res/values-zh/strings.xml
  26. 3
      app/src/main/res/values/ids.xml
  27. 4
      app/src/main/res/values/strings.xml

@ -5,7 +5,7 @@
【温馨提醒】 *本帮助可以在我的-右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!* 【温馨提醒】 *本帮助可以在我的-右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!*
1. 为什么第一次安装好之后什么东西都没有? 1. 为什么第一次安装好之后什么东西都没有?
* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号[开源阅读]()、QQ群、酷安评论里获取由书友制作分享的书源。 * 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号**[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。
2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理? 2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理?
* 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。 * 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。

@ -0,0 +1,20 @@
# 源规则帮助
* [书源帮助文档](https://alanskycn.gitee.io/teachme/Rule/source.html)
* [订阅源帮助文档](https://alanskycn.gitee.io/teachme/Rule/rss.html)
* 规则标志, {{......}}内使用规则必须有明显的规则标志,没有规则标志当作js执行
* @@ 默认规则,直接写时可以省略@@
* @XPath: xpath规则,直接写时以//开头可省略@XPath
* @Json: json规则,直接写时以$.开头可省略@Json
* : regex规则,不可省略,只可以用在书籍列表和目录列表
* js 变量和函数
* java 变量-当前类
* baseUrl 变量-当前url,String
* result 变量-上一步的结果
* book 变量-书籍类,方法见 io.legado.app.data.entities.Book
* cookie 变量-cookie操作类,方法见 io.legado.app.help.http.CookieStore
* cache 变量-缓存操作类,方法见 io.legado.app.help.CacheManager
* chapter 变量-当前目录类,方法见 io.legado.app.data.entities.BookChapter
* title 变量-当前标题,String

@ -1,8 +1,13 @@
# 更新日志 # 更新日志
* 关注公众号 **[开源阅读]()** 菜单•软件下载 提前享受新版本。 * 关注公众号 **[开源阅读]** 菜单•软件下载 提前享受新版本。
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/11/22**
* 正文添加正确字体规则,可以通过文字轮廓对比将错误的文字替换为正确的文字
* js添加java.readFile("path")读取本地文件,返回BiteArray
* js添加java.queryTTF(font: ByteArray?),返回字体处理类,可以编码和轮廓互查,io.legado.app.model.analyzeRule.QueryTTF
**2020/11/18** **2020/11/18**
* 优化导航栏 * 优化导航栏
* js添加java.log(msg: String)用于调试时输出消息 * js添加java.log(msg: String)用于调试时输出消息

@ -36,9 +36,9 @@ object AppConst {
val keyboardToolChars: List<String> by lazy { val keyboardToolChars: List<String> by lazy {
arrayListOf( arrayListOf(
"", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "", "@", "&", "|", "%", "/", "\\", ":", "[", "]", "{", "}", "<", ">",
"$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css", "$", "#", "!", ".", "+", "-", "*", "=", "href", "src", "textNodes", "xpath", "json",
"id", "class", "tag" "css", "id", "class", "tag"
) )
} }

@ -9,8 +9,6 @@ data class ContentRule(
var nextContentUrl: String? = null, var nextContentUrl: String? = null,
var webJs: String? = null, var webJs: String? = null,
var sourceRegex: String? = null, var sourceRegex: String? = null,
var replaceRegex: String? = null, var replaceRegex: String? = null, //正文获取后处理规则
var imageStyle: String? = null, //默认大小居中,FULL最大宽度 var imageStyle: String? = null, //默认大小居中,FULL最大宽度
var font: String? = null, //网页内包含的字体必须返回ByteArray
var correctFont: String? = null, //正确的字体必须返回ByteArray
) : Parcelable ) : Parcelable

@ -2,18 +2,29 @@ package io.legado.app.help
import io.legado.app.App import io.legado.app.App
import io.legado.app.data.entities.Cache import io.legado.app.data.entities.Cache
import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.utils.ACache
@Suppress("unused") @Suppress("unused")
object CacheManager { object CacheManager {
private val queryTTFMap = hashMapOf<String, Pair<Long, QueryTTF>>()
/** /**
* saveTime 单位为秒 * saveTime 单位为秒
*/ */
@JvmOverloads @JvmOverloads
fun put(key: String, value: Any, saveTime: Int = 0) { fun put(key: String, value: Any, saveTime: Int = 0) {
val deadline = if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 val deadline =
val cache = Cache(key, value.toString(), deadline) if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000
App.db.cacheDao().insert(cache) when (value) {
is QueryTTF -> queryTTFMap[key] = Pair(deadline, value)
is ByteArray -> ACache.get(App.INSTANCE).put(key, value, saveTime)
else -> {
val cache = Cache(key, value.toString(), deadline)
App.db.cacheDao().insert(cache)
}
}
} }
fun get(key: String): String? { fun get(key: String): String? {
@ -36,4 +47,15 @@ object CacheManager {
return get(key)?.toFloatOrNull() return get(key)?.toFloatOrNull()
} }
fun getByteArray(key: String): ByteArray? {
return ACache.get(App.INSTANCE).getAsBinary(key)
}
fun getQueryTTF(key: String): QueryTTF? {
val cache = queryTTFMap[key] ?: return null
if (cache.first == 0L || cache.first > System.currentTimeMillis()) {
return cache.second
}
return null
}
} }

@ -1,14 +1,18 @@
package io.legado.app.help package io.legado.app.help
import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.App
import io.legado.app.constant.AppConst.dateFormat import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.help.http.CookieStore import io.legado.app.help.http.CookieStore
import io.legado.app.help.http.HttpHelper
import io.legado.app.help.http.SSLHelper import io.legado.app.help.http.SSLHelper
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.runBlocking
import org.jsoup.Connection import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.io.File import java.io.File
@ -230,11 +234,51 @@ interface JsExtensions {
/** /**
* 解析字体,返回字体解析类 * 解析字体,返回字体解析类
*/ */
fun queryTTF(font: ByteArray?): QueryTTF? { fun queryBase64TTF(base64: String?): QueryTTF? {
base64DecodeToByteArray(base64)?.let {
return QueryTTF(it)
}
return null
}
fun queryTTF(path: String): QueryTTF? {
val qTTF = CacheManager.getQueryTTF(path)
if (qTTF != null) {
return qTTF
}
val font: ByteArray? = when {
path.isAbsUrl() -> runBlocking {
var x = CacheManager.getByteArray(path)
if (x == null) {
x = HttpHelper.simpleGetBytesAsync(path)
x?.let {
CacheManager.put(path, it)
}
}
return@runBlocking x
}
path.isContentScheme() -> {
Uri.parse(path).readBytes(App.INSTANCE)
}
else -> {
File(path).readBytes()
}
}
font ?: return null font ?: return null
return QueryTTF(font) return QueryTTF(font)
} }
fun replaceFont(text: String, font1: QueryTTF, font2: QueryTTF, start: Int, end: Int): String {
val contentArray = text.toCharArray()
contentArray.forEachIndexed { index, s ->
if (s > start.toChar() && s < end.toChar()) {
val code = font2.GetCodeByGlyf(font1.GetGlyfByCode(s.toInt()))
contentArray[index] = code.toChar()
}
}
return contentArray.joinToString("")
}
/** /**
* 输出调试日志 * 输出调试日志
*/ */

@ -39,6 +39,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
private var objectChangedJS = false private var objectChangedJS = false
private var objectChangedJP = false private var objectChangedJP = false
@JvmOverloads
fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule { fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule {
if (content == null) throw AssertionError("Content cannot be null") if (content == null) throw AssertionError("Content cannot be null")
this.content = content this.content = content
@ -297,8 +298,8 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
return ArrayList() return ArrayList()
} }
fun getByteArray(ruleStr: String): ByteArray? { fun getContent(ruleStr: String, text: String?): String {
if (ruleStr.isEmpty()) return null if (ruleStr.isEmpty()) return ""
val ruleList = splitSourceRule(ruleStr) val ruleList = splitSourceRule(ruleStr)
var result: Any? = null var result: Any? = null
content?.let { o -> content?.let { o ->
@ -311,7 +312,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
result.toString(), result.toString(),
sourceRule.rule.splitNotBlank("&&") sourceRule.rule.splitNotBlank("&&")
) )
Mode.Js -> evalJS(sourceRule.rule, result) Mode.Js -> evalJS(sourceRule.rule, result, text)
Mode.Json -> getAnalyzeByJSonPath(it).getList(sourceRule.rule) Mode.Json -> getAnalyzeByJSonPath(it).getList(sourceRule.rule)
Mode.XPath -> getAnalyzeByXPath(it).getElements(sourceRule.rule) Mode.XPath -> getAnalyzeByXPath(it).getElements(sourceRule.rule)
else -> getAnalyzeByJSoup(it).getElements(sourceRule.rule) else -> getAnalyzeByJSoup(it).getElements(sourceRule.rule)
@ -322,7 +323,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
} }
} }
} }
return result as? ByteArray return result?.toString() ?: ""
} }
/** /**
@ -648,7 +649,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
/** /**
* 执行JS * 执行JS
*/ */
private fun evalJS(jsStr: String, result: Any?): Any? { private fun evalJS(jsStr: String, result: Any?, content: String? = null): Any? {
val bindings = SimpleBindings() val bindings = SimpleBindings()
bindings["java"] = this bindings["java"] = this
bindings["cookie"] = CookieStore bindings["cookie"] = CookieStore
@ -658,6 +659,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
bindings["baseUrl"] = baseUrl bindings["baseUrl"] = baseUrl
bindings["chapter"] = chapter bindings["chapter"] = chapter
bindings["title"] = chapter?.title bindings["title"] = chapter?.title
bindings["content"] = content
return SCRIPT_ENGINE.eval(jsStr, bindings) return SCRIPT_ENGINE.eval(jsStr, bindings)
} }

@ -10,10 +10,8 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.htmlFormat import io.legado.app.utils.htmlFormat
import io.legado.app.utils.toStringArray
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -37,21 +35,6 @@ object BookContent {
val nextUrlList = arrayListOf(baseUrl) val nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule() val contentRule = bookSource.getContentRule()
val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl) val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl)
val fontRule = contentRule.font
val correctFontRule = contentRule.correctFont
var font: ByteArray? = null
var correctFont: ByteArray? = null
fontRule?.let {
//todo 获取网页嵌入字体
font = analyzeRule.getByteArray(it)
}
correctFontRule?.let {
//todo 获取正确字体
correctFont = analyzeRule.getByteArray(it)
}
if (correctFont == null && font != null) {
BookHelp.saveFont(book, bookChapter, font!!)
}
var contentData = analyzeContent( var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource book, baseUrl, body, contentRule, bookChapter, bookSource
) )
@ -111,21 +94,7 @@ object BookContent {
var contentStr = content.toString().htmlFormat() var contentStr = content.toString().htmlFormat()
val replaceRegex = bookSource.ruleContent?.replaceRegex val replaceRegex = bookSource.ruleContent?.replaceRegex
if (!replaceRegex.isNullOrEmpty()) { if (!replaceRegex.isNullOrEmpty()) {
analyzeRule.setContent(contentStr).setBaseUrl(baseUrl) contentStr = analyzeRule.getContent(replaceRegex, contentStr)
analyzeRule.chapter = bookChapter
contentStr = analyzeRule.getString(replaceRegex)
}
if (correctFont != null && font != null) {
val queryTTF = QueryTTF(font!!)
val cQueryTTF = QueryTTF(correctFont!!)
val contentArray = contentStr.toCharArray()
contentArray.forEachIndexed { index, s ->
if(s> 58000.toChar()){
val code = cQueryTTF.GetCodeByGlyf(queryTTF.GetGlyfByCode(s.toInt()))
contentArray[index] = code.toChar()
}
}
contentStr = contentArray.joinToString("")
} }
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")

@ -191,12 +191,7 @@ object ReadBook {
if (book != null && textChapter != null) { if (book != null && textChapter != null) {
val key = IntentDataHelp.putData(textChapter) val key = IntentDataHelp.putData(textChapter)
ReadAloud.play( ReadAloud.play(
App.INSTANCE, App.INSTANCE, book.name, textChapter.title, durPageIndex, key, play
book.name,
textChapter.title,
durPageIndex,
key,
play
) )
} }
} }
@ -275,10 +270,7 @@ object ReadBook {
CacheBook.download(webBook, book, chapter) CacheBook.download(webBook, book, chapter)
} else if (book != null) { } else if (book != null) {
contentLoadFinish( contentLoadFinish(
book, book, chapter, "没有书源", resetPageOffset = resetPageOffset
chapter,
"没有书源",
resetPageOffset = resetPageOffset
) )
removeLoading(chapter.index) removeLoading(chapter.index)
} else { } else {
@ -383,11 +375,7 @@ object ReadBook {
durChapterIndex -> { durChapterIndex -> {
curTextChapter = curTextChapter =
ChapterProvider.getTextChapter( ChapterProvider.getTextChapter(
book, book, chapter, contents, chapterSize, imageStyle
chapter,
contents,
chapterSize,
imageStyle
) )
if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset)
callBack?.upView() callBack?.upView()
@ -397,22 +385,14 @@ object ReadBook {
durChapterIndex - 1 -> { durChapterIndex - 1 -> {
prevTextChapter = prevTextChapter =
ChapterProvider.getTextChapter( ChapterProvider.getTextChapter(
book, book, chapter, contents, chapterSize, imageStyle
chapter,
contents,
chapterSize,
imageStyle
) )
if (upContent) callBack?.upContent(-1, resetPageOffset) if (upContent) callBack?.upContent(-1, resetPageOffset)
} }
durChapterIndex + 1 -> { durChapterIndex + 1 -> {
nextTextChapter = nextTextChapter =
ChapterProvider.getTextChapter( ChapterProvider.getTextChapter(
book, book, chapter, contents, chapterSize, imageStyle
chapter,
contents,
chapterSize,
imageStyle
) )
if (upContent) callBack?.upContent(1, resetPageOffset) if (upContent) callBack?.upContent(1, resetPageOffset)
} }

@ -37,7 +37,7 @@ import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR
import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.TextPageFactory import io.legado.app.ui.book.read.page.TextPageFactory
import io.legado.app.ui.book.read.page.delegate.PageDelegate import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.searchContent.SearchContentActivity
import io.legado.app.ui.book.toc.ChapterListActivity import io.legado.app.ui.book.toc.ChapterListActivity
import io.legado.app.ui.login.SourceLogin import io.legado.app.ui.login.SourceLogin
@ -260,28 +260,28 @@ class ReadBookActivity : ReadBookBaseActivity(),
when { when {
isPrevKey(keyCode) -> { isPrevKey(keyCode) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV) page_view.pageDelegate?.keyTurnPage(PageDirection.PREV)
return true return true
} }
} }
isNextKey(keyCode) -> { isNextKey(keyCode) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) page_view.pageDelegate?.keyTurnPage(PageDirection.NEXT)
return true return true
} }
} }
keyCode == KeyEvent.KEYCODE_VOLUME_UP -> { keyCode == KeyEvent.KEYCODE_VOLUME_UP -> {
if (volumeKeyPage(PageDelegate.Direction.PREV)) { if (volumeKeyPage(PageDirection.PREV)) {
return true return true
} }
} }
keyCode == KeyEvent.KEYCODE_VOLUME_DOWN -> { keyCode == KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (volumeKeyPage(PageDelegate.Direction.NEXT)) { if (volumeKeyPage(PageDirection.NEXT)) {
return true return true
} }
} }
keyCode == KeyEvent.KEYCODE_SPACE -> { keyCode == KeyEvent.KEYCODE_SPACE -> {
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) page_view.pageDelegate?.keyTurnPage(PageDirection.NEXT)
return true return true
} }
} }
@ -307,7 +307,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (volumeKeyPage(PageDelegate.Direction.NONE)) { if (volumeKeyPage(PageDirection.NONE)) {
return true return true
} }
} }
@ -458,7 +458,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
/** /**
* 音量键翻页 * 音量键翻页
*/ */
private fun volumeKeyPage(direction: PageDelegate.Direction): Boolean { private fun volumeKeyPage(direction: PageDirection): Boolean {
if (!read_menu.isVisible) { if (!read_menu.isVisible) {
if (getPrefBoolean("volumeKeyPage", true)) { if (getPrefBoolean("volumeKeyPage", true)) {
if (getPrefBoolean("volumeKeyPageOnPlay") if (getPrefBoolean("volumeKeyPageOnPlay")
@ -612,7 +612,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
autoPageProgress++ autoPageProgress++
if (autoPageProgress >= ReadBookConfig.autoReadSpeed * 50) { if (autoPageProgress >= ReadBookConfig.autoReadSpeed * 50) {
autoPageProgress = 0 autoPageProgress = 0
page_view.fillPage(PageDelegate.Direction.NEXT) page_view.fillPage(PageDirection.NEXT)
} else { } else {
page_view.invalidate() page_view.invalidate()
} }

@ -15,6 +15,7 @@ import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.delegate.* import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.activity import io.legado.app.utils.activity
@ -339,12 +340,12 @@ class PageView(context: Context, attrs: AttributeSet) :
curPage.cancelSelect() curPage.cancelSelect()
} }
fun fillPage(direction: PageDelegate.Direction) { fun fillPage(direction: PageDirection) {
when (direction) { when (direction) {
PageDelegate.Direction.PREV -> { PageDirection.PREV -> {
pageFactory.moveToPrev(true) pageFactory.moveToPrev(true)
} }
PageDelegate.Direction.NEXT -> { PageDirection.NEXT -> {
pageFactory.moveToNext(true) pageFactory.moveToNext(true)
} }
else -> Unit else -> Unit

@ -4,6 +4,7 @@ import android.graphics.Canvas
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.entities.PageDirection
class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
private val bitmapMatrix = Matrix() private val bitmapMatrix = Matrix()
@ -21,19 +22,19 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
if (!isRunning) return if (!isRunning) return
val offsetX = touchX - startX val offsetX = touchX - startX
if ((mDirection == Direction.NEXT && offsetX > 0) if ((mDirection == PageDirection.NEXT && offsetX > 0)
|| (mDirection == Direction.PREV && offsetX < 0) || (mDirection == PageDirection.PREV && offsetX < 0)
) { ) {
return return
} }
val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
if (mDirection == Direction.PREV) { if (mDirection == PageDirection.PREV) {
bitmapMatrix.setTranslate(distanceX, 0.toFloat()) bitmapMatrix.setTranslate(distanceX, 0.toFloat())
curBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } curBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
addShadow(distanceX.toInt(), canvas) addShadow(distanceX.toInt(), canvas)
} else if (mDirection == Direction.NEXT) { } else if (mDirection == PageDirection.NEXT) {
bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat())
nextBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } nextBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
@ -60,7 +61,7 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
override fun onAnimStart(animationSpeed: Int) { override fun onAnimStart(animationSpeed: Int) {
val distanceX: Float val distanceX: Float
when (mDirection) { when (mDirection) {
Direction.NEXT -> distanceX = PageDirection.NEXT -> distanceX =
if (isCancel) { if (isCancel) {
var dis = viewWidth - startX + touchX var dis = viewWidth - startX + touchX
if (dis > viewWidth) { if (dis > viewWidth) {

@ -3,6 +3,7 @@ package io.legado.app.ui.book.read.page.delegate
import android.graphics.Bitmap import android.graphics.Bitmap
import android.view.MotionEvent import android.view.MotionEvent
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.utils.screenshot import io.legado.app.utils.screenshot
abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) { abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) {
@ -11,20 +12,20 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
protected var prevBitmap: Bitmap? = null protected var prevBitmap: Bitmap? = null
protected var nextBitmap: Bitmap? = null protected var nextBitmap: Bitmap? = null
override fun setDirection(direction: Direction) { override fun setDirection(direction: PageDirection) {
super.setDirection(direction) super.setDirection(direction)
setBitmap() setBitmap()
} }
private fun setBitmap() { private fun setBitmap() {
when (mDirection) { when (mDirection) {
Direction.PREV -> { PageDirection.PREV -> {
prevBitmap?.recycle() prevBitmap?.recycle()
prevBitmap = prevPage.screenshot() prevBitmap = prevPage.screenshot()
curBitmap?.recycle() curBitmap?.recycle()
curBitmap = curPage.screenshot() curBitmap = curPage.screenshot()
} }
Direction.NEXT -> { PageDirection.NEXT -> {
nextBitmap?.recycle() nextBitmap?.recycle()
nextBitmap = nextPage.screenshot() nextBitmap = nextPage.screenshot()
curBitmap?.recycle() curBitmap?.recycle()
@ -79,19 +80,19 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
noNext = true noNext = true
return return
} }
setDirection(Direction.PREV) setDirection(PageDirection.PREV)
} else { } else {
//如果不存在表示没有下一页了 //如果不存在表示没有下一页了
if (!hasNext()) { if (!hasNext()) {
noNext = true noNext = true
return return
} }
setDirection(Direction.NEXT) setDirection(PageDirection.NEXT)
} }
} }
} }
if (isMoved) { if (isMoved) {
isCancel = if (mDirection == Direction.NEXT) sumX > lastX else sumX < lastX isCancel = if (mDirection == PageDirection.NEXT) sumX > lastX else sumX < lastX
isRunning = true isRunning = true
//设置触摸点 //设置触摸点
pageView.setTouchPoint(sumX, sumY) pageView.setTouchPoint(sumX, sumY)
@ -117,7 +118,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
override fun nextPageByAnim(animationSpeed: Int) { override fun nextPageByAnim(animationSpeed: Int) {
abortAnim() abortAnim()
if (!hasNext()) return if (!hasNext()) return
setDirection(Direction.NEXT) setDirection(PageDirection.NEXT)
pageView.setTouchPoint(viewWidth.toFloat(), 0f, false) pageView.setTouchPoint(viewWidth.toFloat(), 0f, false)
onAnimStart(animationSpeed) onAnimStart(animationSpeed)
} }
@ -125,7 +126,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
override fun prevPageByAnim(animationSpeed: Int) { override fun prevPageByAnim(animationSpeed: Int) {
abortAnim() abortAnim()
if (!hasPrev()) return if (!hasPrev()) return
setDirection(Direction.PREV) setDirection(PageDirection.PREV)
pageView.setTouchPoint(0f, 0f) pageView.setTouchPoint(0f, 0f)
onAnimStart(animationSpeed) onAnimStart(animationSpeed)
} }

@ -10,6 +10,7 @@ import com.google.android.material.snackbar.Snackbar
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.entities.PageDirection
import kotlin.math.abs import kotlin.math.abs
abstract class PageDelegate(protected val pageView: PageView) { abstract class PageDelegate(protected val pageView: PageView) {
@ -47,7 +48,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
var noNext = true var noNext = true
//移动方向 //移动方向
var mDirection = Direction.NONE var mDirection = PageDirection.NONE
var isCancel = false var isCancel = false
var isRunning = false var isRunning = false
var isStarted = false var isStarted = false
@ -117,17 +118,17 @@ abstract class PageDelegate(protected val pageView: PageView) {
abstract fun prevPageByAnim(animationSpeed: Int) abstract fun prevPageByAnim(animationSpeed: Int)
open fun keyTurnPage(direction: Direction) { open fun keyTurnPage(direction: PageDirection) {
if (isRunning) return if (isRunning) return
when (direction) { when (direction) {
Direction.NEXT -> nextPageByAnim(100) PageDirection.NEXT -> nextPageByAnim(100)
Direction.PREV -> prevPageByAnim(100) PageDirection.PREV -> prevPageByAnim(100)
else -> return else -> return
} }
} }
@CallSuper @CallSuper
open fun setDirection(direction: Direction) { open fun setDirection(direction: PageDirection) {
mDirection = direction mDirection = direction
} }
@ -149,7 +150,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
//取消 //取消
isCancel = false isCancel = false
//是下一章还是前一章 //是下一章还是前一章
setDirection(Direction.NONE) setDirection(PageDirection.NONE)
} }
/** /**
@ -184,8 +185,4 @@ abstract class PageDelegate(protected val pageView: PageView) {
} }
enum class Direction {
NONE, PREV, NEXT
}
} }

@ -6,6 +6,7 @@ import android.os.Build
import android.view.MotionEvent import android.view.MotionEvent
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.entities.PageDirection
import kotlin.math.* import kotlin.math.*
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -123,13 +124,13 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3) if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3)
|| mDirection == Direction.PREV || mDirection == PageDirection.PREV
) { ) {
pageView.touchY = viewHeight.toFloat() pageView.touchY = viewHeight.toFloat()
} }
if (startY > viewHeight / 3 && startY < viewHeight / 2 if (startY > viewHeight / 3 && startY < viewHeight / 2
&& mDirection == Direction.NEXT && mDirection == PageDirection.NEXT
) { ) {
pageView.touchY = 1f pageView.touchY = 1f
} }
@ -137,17 +138,17 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} }
} }
override fun setDirection(direction: Direction) { override fun setDirection(direction: PageDirection) {
super.setDirection(direction) super.setDirection(direction)
when (direction) { when (direction) {
Direction.PREV -> PageDirection.PREV ->
//上一页滑动不出现对角 //上一页滑动不出现对角
if (startX > viewWidth / 2) { if (startX > viewWidth / 2) {
calcCornerXY(startX, viewHeight.toFloat()) calcCornerXY(startX, viewHeight.toFloat())
} else { } else {
calcCornerXY(viewWidth - startX, viewHeight.toFloat()) calcCornerXY(viewWidth - startX, viewHeight.toFloat())
} }
Direction.NEXT -> PageDirection.NEXT ->
if (viewWidth / 2 > startX) { if (viewWidth / 2 > startX) {
calcCornerXY(viewWidth - startX, startY) calcCornerXY(viewWidth - startX, startY)
} }
@ -160,12 +161,12 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
val dy: Float val dy: Float
// dy 垂直方向滑动的距离,负值会使滚动向上滚动 // dy 垂直方向滑动的距离,负值会使滚动向上滚动
if (isCancel) { if (isCancel) {
dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) {
(viewWidth - touchX) (viewWidth - touchX)
} else { } else {
-touchX -touchX
} }
if (mDirection != Direction.NEXT) { if (mDirection != PageDirection.NEXT) {
dx = -(viewWidth + touchX) dx = -(viewWidth + touchX)
} }
dy = if (mCornerY > 0) { dy = if (mCornerY > 0) {
@ -174,7 +175,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
-touchY // 防止mTouchY最终变为0 -touchY // 防止mTouchY最终变为0
} }
} else { } else {
dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) {
-(viewWidth + touchX) -(viewWidth + touchX)
} else { } else {
(viewWidth - touchX + viewWidth) (viewWidth - touchX + viewWidth)
@ -197,14 +198,14 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (!isRunning) return if (!isRunning) return
when (mDirection) { when (mDirection) {
Direction.NEXT -> { PageDirection.NEXT -> {
calcPoints() calcPoints()
drawCurrentPageArea(canvas, curBitmap) drawCurrentPageArea(canvas, curBitmap)
drawNextPageAreaAndShadow(canvas, nextBitmap) drawNextPageAreaAndShadow(canvas, nextBitmap)
drawCurrentPageShadow(canvas) drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, curBitmap) drawCurrentBackArea(canvas, curBitmap)
} }
Direction.PREV -> { PageDirection.PREV -> {
calcPoints() calcPoints()
drawCurrentPageArea(canvas, prevBitmap) drawCurrentPageArea(canvas, prevBitmap)
drawNextPageAreaAndShadow(canvas, curBitmap) drawNextPageAreaAndShadow(canvas, curBitmap)

@ -3,6 +3,7 @@ package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Matrix import android.graphics.Matrix
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.entities.PageDirection
class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
@ -11,7 +12,7 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
override fun onAnimStart(animationSpeed: Int) { override fun onAnimStart(animationSpeed: Int) {
val distanceX: Float val distanceX: Float
when (mDirection) { when (mDirection) {
Direction.NEXT -> distanceX = PageDirection.NEXT -> distanceX =
if (isCancel) { if (isCancel) {
var dis = viewWidth - startX + touchX var dis = viewWidth - startX + touchX
if (dis > viewWidth) { if (dis > viewWidth) {
@ -34,17 +35,17 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
val offsetX = touchX - startX val offsetX = touchX - startX
if ((mDirection == Direction.NEXT && offsetX > 0) if ((mDirection == PageDirection.NEXT && offsetX > 0)
|| (mDirection == Direction.PREV && offsetX < 0) || (mDirection == PageDirection.PREV && offsetX < 0)
) return ) return
val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
if (!isRunning) return if (!isRunning) return
if (mDirection == Direction.PREV) { if (mDirection == PageDirection.PREV) {
bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat()) bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat())
curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
bitmapMatrix.setTranslate(distanceX, 0.toFloat()) bitmapMatrix.setTranslate(distanceX, 0.toFloat())
prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
} else if (mDirection == Direction.NEXT) { } else if (mDirection == PageDirection.NEXT) {
bitmapMatrix.setTranslate(distanceX, 0.toFloat()) bitmapMatrix.setTranslate(distanceX, 0.toFloat())
nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat())

@ -0,0 +1,5 @@
package io.legado.app.ui.book.read.page.entities
enum class PageDirection {
NONE, PREV, NEXT
}

@ -237,8 +237,6 @@ class BookSourceEditActivity :
add(EditEntity("sourceRegex", cr?.sourceRegex, R.string.rule_source_regex)) add(EditEntity("sourceRegex", cr?.sourceRegex, R.string.rule_source_regex))
add(EditEntity("replaceRegex", cr?.replaceRegex, R.string.rule_replace_regex)) add(EditEntity("replaceRegex", cr?.replaceRegex, R.string.rule_replace_regex))
add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style)) add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style))
add(EditEntity("font", cr?.font, R.string.rule_font))
add(EditEntity("correctFont", cr?.correctFont, R.string.rule_correct_font))
} }
//发现 //发现
val er = source?.getExploreRule() val er = source?.getExploreRule()
@ -342,8 +340,6 @@ class BookSourceEditActivity :
"sourceRegex" -> contentRule.sourceRegex = it.value "sourceRegex" -> contentRule.sourceRegex = it.value
"replaceRegex" -> contentRule.replaceRegex = it.value "replaceRegex" -> contentRule.replaceRegex = it.value
"imageStyle" -> contentRule.imageStyle = it.value "imageStyle" -> contentRule.imageStyle = it.value
"font" -> contentRule.font = it.value
"correctFont" -> contentRule.correctFont = it.value
} }
} }
source.ruleSearch = searchRule source.ruleSearch = searchRule
@ -390,13 +386,18 @@ class BookSourceEditActivity :
selector(getString(R.string.help), items) { _, index -> selector(getString(R.string.help), items) { _, index ->
when (index) { when (index) {
0 -> insertText(AppConst.urlOption) 0 -> insertText(AppConst.urlOption)
1 -> openUrl("https://alanskycn.gitee.io/teachme/Rule/source.html") 1 -> showSourceHelp()
2 -> showRegexHelp() 2 -> showRegexHelp()
3 -> FilePicker.selectFile(this, selectPathRequestCode) 3 -> FilePicker.selectFile(this, selectPathRequestCode)
} }
} }
} }
private fun showSourceHelp() {
val mdText = String(assets.open("help/sourceHelp.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)
}
private fun showRegexHelp() { private fun showRegexHelp() {
val mdText = String(assets.open("help/regexHelp.md").readBytes()) val mdText = String(assets.open("help/regexHelp.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)

@ -21,7 +21,10 @@ import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.* import io.legado.app.utils.GSON
import io.legado.app.utils.getViewModel
import io.legado.app.utils.sendToClip
import io.legado.app.utils.shareWithQr
import kotlinx.android.synthetic.main.activity_rss_source_edit.* import kotlinx.android.synthetic.main.activity_rss_source_edit.*
import org.jetbrains.anko.* import org.jetbrains.anko.*
import kotlin.math.abs import kotlin.math.abs
@ -199,12 +202,17 @@ class RssSourceEditActivity :
selector(getString(R.string.help), items) { _, index -> selector(getString(R.string.help), items) { _, index ->
when (index) { when (index) {
0 -> insertText(AppConst.urlOption) 0 -> insertText(AppConst.urlOption)
1 -> openUrl("https://alanskycn.gitee.io/teachme/Rule/rss.html") 1 -> showSourceHelp()
2 -> showRegexHelp() 2 -> showRegexHelp()
} }
} }
} }
private fun showSourceHelp() {
val mdText = String(assets.open("help/sourceHelp.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)
}
private fun showRegexHelp() { private fun showRegexHelp() {
val mdText = String(assets.open("help/regexHelp.md").readBytes()) val mdText = String(assets.open("help/regexHelp.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)

@ -0,0 +1,437 @@
package io.legado.app.ui.widget.text
import android.app.Activity
import android.graphics.RectF
import android.text.Selection
import android.text.Spannable
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.BackgroundColorSpan
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.text.util.Linkify
import android.view.*
import android.widget.TextView
import io.legado.app.R
import io.legado.app.ui.widget.text.BetterLinkMovementMethod.LongPressTimer.OnTimerReachedListener
class BetterLinkMovementMethod protected constructor() : LinkMovementMethod() {
private var onLinkClickListener: OnLinkClickListener? = null
private var onLinkLongClickListener: OnLinkLongClickListener? = null
private val touchedLineBounds = RectF()
private var isUrlHighlighted = false
private var clickableSpanUnderTouchOnActionDown: ClickableSpan? = null
private var activeTextViewHashcode = 0
private var ongoingLongPressTimer: LongPressTimer? = null
private var wasLongPressRegistered = false
interface OnLinkClickListener {
/**
* @param textView The TextView on which a click was registered.
* @param url The clicked URL.
* @return True if this click was handled. False to let Android handle the URL.
*/
fun onClick(textView: TextView?, url: String?): Boolean
}
interface OnLinkLongClickListener {
/**
* @param textView The TextView on which a long-click was registered.
* @param url The long-clicked URL.
* @return True if this long-click was handled. False to let Android handle the URL (as a short-click).
*/
fun onLongClick(textView: TextView?, url: String?): Boolean
}
/**
* Set a listener that will get called whenever any link is clicked on the TextView.
*/
fun setOnLinkClickListener(clickListener: OnLinkClickListener?): BetterLinkMovementMethod {
if (this === singleInstance) {
throw UnsupportedOperationException(
"Setting a click listener on the instance returned by getInstance() is not supported to avoid memory " +
"leaks. Please use newInstance() or any of the linkify() methods instead."
)
}
onLinkClickListener = clickListener
return this
}
/**
* Set a listener that will get called whenever any link is clicked on the TextView.
*/
fun setOnLinkLongClickListener(longClickListener: OnLinkLongClickListener?): BetterLinkMovementMethod {
if (this === singleInstance) {
throw UnsupportedOperationException(
"Setting a long-click listener on the instance returned by getInstance() is not supported to avoid " +
"memory leaks. Please use newInstance() or any of the linkify() methods instead."
)
}
onLinkLongClickListener = longClickListener
return this
}
override fun onTouchEvent(textView: TextView, text: Spannable, event: MotionEvent): Boolean {
if (activeTextViewHashcode != textView.hashCode()) {
// Bug workaround: TextView stops calling onTouchEvent() once any URL is highlighted.
// A hacky solution is to reset any "autoLink" property set in XML. But we also want
// to do this once per TextView.
activeTextViewHashcode = textView.hashCode()
textView.autoLinkMask = 0
}
val clickableSpanUnderTouch = findClickableSpanUnderTouch(textView, text, event)
if (event.action == MotionEvent.ACTION_DOWN) {
clickableSpanUnderTouchOnActionDown = clickableSpanUnderTouch
}
val touchStartedOverAClickableSpan = clickableSpanUnderTouchOnActionDown != null
return when (event.action) {
MotionEvent.ACTION_DOWN -> {
clickableSpanUnderTouch?.let { highlightUrl(textView, it, text) }
if (touchStartedOverAClickableSpan && onLinkLongClickListener != null) {
val longClickListener: OnTimerReachedListener =
object : OnTimerReachedListener {
override fun onTimerReached() {
wasLongPressRegistered = true
textView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
removeUrlHighlightColor(textView)
dispatchUrlLongClick(textView, clickableSpanUnderTouch)
}
}
startTimerForRegisteringLongClick(textView, longClickListener)
}
touchStartedOverAClickableSpan
}
MotionEvent.ACTION_UP -> {
// Register a click only if the touch started and ended on the same URL.
if (!wasLongPressRegistered && touchStartedOverAClickableSpan && clickableSpanUnderTouch === clickableSpanUnderTouchOnActionDown) {
dispatchUrlClick(textView, clickableSpanUnderTouch)
}
cleanupOnTouchUp(textView)
// Consume this event even if we could not find any spans to avoid letting Android handle this event.
// Android's TextView implementation has a bug where links get clicked even when there is no more text
// next to the link and the touch lies outside its bounds in the same direction.
touchStartedOverAClickableSpan
}
MotionEvent.ACTION_CANCEL -> {
cleanupOnTouchUp(textView)
false
}
MotionEvent.ACTION_MOVE -> {
// Stop listening for a long-press as soon as the user wanders off to unknown lands.
if (clickableSpanUnderTouch !== clickableSpanUnderTouchOnActionDown) {
removeLongPressCallback(textView)
}
if (!wasLongPressRegistered) {
// Toggle highlight.
if (clickableSpanUnderTouch != null) {
highlightUrl(textView, clickableSpanUnderTouch, text)
} else {
removeUrlHighlightColor(textView)
}
}
touchStartedOverAClickableSpan
}
else -> false
}
}
private fun cleanupOnTouchUp(textView: TextView) {
wasLongPressRegistered = false
clickableSpanUnderTouchOnActionDown = null
removeUrlHighlightColor(textView)
removeLongPressCallback(textView)
}
/**
* Determines the touched location inside the TextView's text and returns the ClickableSpan found under it (if any).
*
* @return The touched ClickableSpan or null.
*/
protected fun findClickableSpanUnderTouch(
textView: TextView,
text: Spannable,
event: MotionEvent
): ClickableSpan? {
// So we need to find the location in text where touch was made, regardless of whether the TextView
// has scrollable text. That is, not the entire text is currently visible.
var touchX = event.x.toInt()
var touchY = event.y.toInt()
// Ignore padding.
touchX -= textView.totalPaddingLeft
touchY -= textView.totalPaddingTop
// Account for scrollable text.
touchX += textView.scrollX
touchY += textView.scrollY
val layout = textView.layout
val touchedLine = layout.getLineForVertical(touchY)
val touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX.toFloat())
touchedLineBounds.left = layout.getLineLeft(touchedLine)
touchedLineBounds.top = layout.getLineTop(touchedLine).toFloat()
touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left
touchedLineBounds.bottom = layout.getLineBottom(touchedLine).toFloat()
return if (touchedLineBounds.contains(touchX.toFloat(), touchY.toFloat())) {
// Find a ClickableSpan that lies under the touched area.
val spans = text.getSpans(touchOffset, touchOffset, ClickableSpan::class.java)
for (span in spans) {
if (span is ClickableSpan) {
return span
}
}
// No ClickableSpan found under the touched location.
null
} else {
// Touch lies outside the line's horizontal bounds where no spans should exist.
null
}
}
/**
* Adds a background color span at <var>clickableSpan</var>'s location.
*/
protected fun highlightUrl(textView: TextView, clickableSpan: ClickableSpan?, text: Spannable) {
if (isUrlHighlighted) {
return
}
isUrlHighlighted = true
val spanStart = text.getSpanStart(clickableSpan)
val spanEnd = text.getSpanEnd(clickableSpan)
val highlightSpan = BackgroundColorSpan(textView.highlightColor)
text.setSpan(highlightSpan, spanStart, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
textView.setTag(R.id.bettermovementmethod_highlight_background_span, highlightSpan)
Selection.setSelection(text, spanStart, spanEnd)
}
/**
* Removes the highlight color under the Url.
*/
protected fun removeUrlHighlightColor(textView: TextView) {
if (!isUrlHighlighted) {
return
}
isUrlHighlighted = false
val text = textView.text as Spannable
val highlightSpan =
textView.getTag(R.id.bettermovementmethod_highlight_background_span) as BackgroundColorSpan
text.removeSpan(highlightSpan)
Selection.removeSelection(text)
}
protected fun startTimerForRegisteringLongClick(
textView: TextView,
longClickListener: OnTimerReachedListener?
) {
ongoingLongPressTimer = LongPressTimer()
ongoingLongPressTimer!!.setOnTimerReachedListener(longClickListener)
textView.postDelayed(
ongoingLongPressTimer,
ViewConfiguration.getLongPressTimeout().toLong()
)
}
/**
* Remove the long-press detection timer.
*/
protected fun removeLongPressCallback(textView: TextView) {
if (ongoingLongPressTimer != null) {
textView.removeCallbacks(ongoingLongPressTimer)
ongoingLongPressTimer = null
}
}
protected fun dispatchUrlClick(textView: TextView, clickableSpan: ClickableSpan?) {
val clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan)
val handled = onLinkClickListener != null && onLinkClickListener!!.onClick(
textView,
clickableSpanWithText.text()
)
if (!handled) {
// Let Android handle this click.
clickableSpanWithText.span()!!.onClick(textView)
}
}
protected fun dispatchUrlLongClick(textView: TextView, clickableSpan: ClickableSpan?) {
val clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan)
val handled = onLinkLongClickListener != null && onLinkLongClickListener!!.onLongClick(
textView,
clickableSpanWithText.text()
)
if (!handled) {
// Let Android handle this long click as a short-click.
clickableSpanWithText.span()!!.onClick(textView)
}
}
protected class LongPressTimer : Runnable {
private var onTimerReachedListener: OnTimerReachedListener? = null
interface OnTimerReachedListener {
fun onTimerReached()
}
override fun run() {
onTimerReachedListener!!.onTimerReached()
}
fun setOnTimerReachedListener(listener: OnTimerReachedListener?) {
onTimerReachedListener = listener
}
}
/**
* A wrapper to support all [ClickableSpan]s that may or may not provide URLs.
*/
protected class ClickableSpanWithText protected constructor(
private val span: ClickableSpan?,
private val text: String
) {
fun span(): ClickableSpan? {
return span
}
fun text(): String {
return text
}
companion object {
fun ofSpan(textView: TextView, span: ClickableSpan?): ClickableSpanWithText {
val s = textView.text as Spanned
val text: String
text = if (span is URLSpan) {
span.url
} else {
val start = s.getSpanStart(span)
val end = s.getSpanEnd(span)
s.subSequence(start, end).toString()
}
return ClickableSpanWithText(span, text)
}
}
}
companion object {
private var singleInstance: BetterLinkMovementMethod? = null
private const val LINKIFY_NONE = -2
/**
* Return a new instance of BetterLinkMovementMethod.
*/
fun newInstance(): BetterLinkMovementMethod {
return BetterLinkMovementMethod()
}
/**
* @param linkifyMask One of [Linkify.ALL], [Linkify.PHONE_NUMBERS], [Linkify.MAP_ADDRESSES],
* [Linkify.WEB_URLS] and [Linkify.EMAIL_ADDRESSES].
* @param textViews The TextViews on which a [BetterLinkMovementMethod] should be registered.
* @return The registered [BetterLinkMovementMethod] on the TextViews.
*/
fun linkify(linkifyMask: Int, vararg textViews: TextView): BetterLinkMovementMethod {
val movementMethod = newInstance()
for (textView in textViews) {
addLinks(linkifyMask, movementMethod, textView)
}
return movementMethod
}
/**
* Like [.linkify], but can be used for TextViews with HTML links.
*
* @param textViews The TextViews on which a [BetterLinkMovementMethod] should be registered.
* @return The registered [BetterLinkMovementMethod] on the TextViews.
*/
fun linkifyHtml(vararg textViews: TextView): BetterLinkMovementMethod {
return linkify(LINKIFY_NONE, *textViews)
}
/**
* Recursively register a [BetterLinkMovementMethod] on every TextView inside a layout.
*
* @param linkifyMask One of [Linkify.ALL], [Linkify.PHONE_NUMBERS], [Linkify.MAP_ADDRESSES],
* [Linkify.WEB_URLS] and [Linkify.EMAIL_ADDRESSES].
* @return The registered [BetterLinkMovementMethod] on the TextViews.
*/
fun linkify(linkifyMask: Int, viewGroup: ViewGroup): BetterLinkMovementMethod {
val movementMethod = newInstance()
rAddLinks(linkifyMask, viewGroup, movementMethod)
return movementMethod
}
/**
* Like [.linkify], but can be used for TextViews with HTML links.
*
* @return The registered [BetterLinkMovementMethod] on the TextViews.
*/
fun linkifyHtml(viewGroup: ViewGroup): BetterLinkMovementMethod {
return linkify(LINKIFY_NONE, viewGroup)
}
/**
* Recursively register a [BetterLinkMovementMethod] on every TextView inside a layout.
*
* @param linkifyMask One of [Linkify.ALL], [Linkify.PHONE_NUMBERS], [Linkify.MAP_ADDRESSES],
* [Linkify.WEB_URLS] and [Linkify.EMAIL_ADDRESSES].
* @return The registered [BetterLinkMovementMethod] on the TextViews.
*/
fun linkify(linkifyMask: Int, activity: Activity): BetterLinkMovementMethod {
// Find the layout passed to setContentView().
val activityLayout =
(activity.findViewById<View>(Window.ID_ANDROID_CONTENT) as ViewGroup).getChildAt(0) as ViewGroup
val movementMethod = newInstance()
rAddLinks(linkifyMask, activityLayout, movementMethod)
return movementMethod
}
/**
* Like [.linkify], but can be used for TextViews with HTML links.
*
* @return The registered [BetterLinkMovementMethod] on the TextViews.
*/
fun linkifyHtml(activity: Activity): BetterLinkMovementMethod {
return linkify(LINKIFY_NONE, activity)
}
/**
* Get a static instance of BetterLinkMovementMethod. Do note that registering a click listener on the returned
* instance is not supported because it will potentially be shared on multiple TextViews.
*/
val instance: BetterLinkMovementMethod?
get() {
if (singleInstance == null) {
singleInstance = BetterLinkMovementMethod()
}
return singleInstance
}
// ======== PUBLIC APIs END ======== //
private fun rAddLinks(
linkifyMask: Int,
viewGroup: ViewGroup,
movementMethod: BetterLinkMovementMethod
) {
for (i in 0 until viewGroup.childCount) {
val child = viewGroup.getChildAt(i)
if (child is ViewGroup) {
// Recursively find child TextViews.
rAddLinks(linkifyMask, child, movementMethod)
} else if (child is TextView) {
addLinks(linkifyMask, movementMethod, child)
}
}
}
private fun addLinks(
linkifyMask: Int,
movementMethod: BetterLinkMovementMethod,
textView: TextView
) {
textView.movementMethod = movementMethod
if (linkifyMask != LINKIFY_NONE) {
Linkify.addLinks(textView, linkifyMask)
}
}
}
}

@ -48,6 +48,7 @@ open class InertiaScrollTextView @JvmOverloads constructor(
mTouchSlop = vc.scaledTouchSlop mTouchSlop = vc.scaledTouchSlop
mMinFlingVelocity = vc.scaledMinimumFlingVelocity mMinFlingVelocity = vc.scaledMinimumFlingVelocity
mMaxFlingVelocity = vc.scaledMaximumFlingVelocity mMaxFlingVelocity = vc.scaledMaximumFlingVelocity
movementMethod = BetterLinkMovementMethod.instance
} }
fun atTop(): Boolean { fun atTop(): Boolean {

@ -412,8 +412,6 @@
<string name="rule_next_content">正文下一頁 URL 規則 (nextContentUrl)</string> <string name="rule_next_content">正文下一頁 URL 規則 (nextContentUrl)</string>
<string name="rule_web_js">webJs</string> <string name="rule_web_js">webJs</string>
<string name="rule_source_regex">資源正則 (sourceRegex)</string> <string name="rule_source_regex">資源正則 (sourceRegex)</string>
<string name="rule_font">网页内嵌字體(font)</string>
<string name="rule_correct_font">正确字体(font)</string>
<string name="source_icon">圖標 (sourceIcon)</string> <string name="source_icon">圖標 (sourceIcon)</string>
<string name="r_articles">列表規則 (ruleArticles)</string> <string name="r_articles">列表規則 (ruleArticles)</string>
@ -732,7 +730,7 @@
<string name="restore_ignore_summary">恢復時忽略一些內容不恢復,方便不同手機配置不同</string> <string name="restore_ignore_summary">恢復時忽略一些內容不恢復,方便不同手機配置不同</string>
<string name="read_config">閱讀界面設置</string> <string name="read_config">閱讀界面設置</string>
<string name="rule_image_style">图片样式(imageStyle)</string> <string name="rule_image_style">图片样式(imageStyle)</string>
<string name="rule_replace_regex">替换规则(replaceRegex)</string> <string name="rule_replace_regex">正文获取后处理规则(replaceRegex)</string>
<string name="group_name">分組名稱</string> <string name="group_name">分組名稱</string>
<string name="note_content">備註內容</string> <string name="note_content">備註內容</string>
<string name="replace_enable_default_t">默认启用替换净化</string> <string name="replace_enable_default_t">默认启用替换净化</string>

@ -413,8 +413,6 @@
<string name="rule_next_content">正文下一頁URL規則(nextContentUrl)</string> <string name="rule_next_content">正文下一頁URL規則(nextContentUrl)</string>
<string name="rule_web_js">webJs</string> <string name="rule_web_js">webJs</string>
<string name="rule_source_regex">資源正則(sourceRegex)</string> <string name="rule_source_regex">資源正則(sourceRegex)</string>
<string name="rule_font">网页内嵌字體(font)</string>
<string name="rule_correct_font">正确字体(font)</string>
<string name="source_icon">圖示(sourceIcon)</string> <string name="source_icon">圖示(sourceIcon)</string>
<string name="r_articles">列表規則(ruleArticles)</string> <string name="r_articles">列表規則(ruleArticles)</string>
@ -732,7 +730,7 @@
<string name="restore_ignore_summary">復原時忽略一些內容不復原,方便不同手機配置不同</string> <string name="restore_ignore_summary">復原時忽略一些內容不復原,方便不同手機配置不同</string>
<string name="read_config">閱讀介面設定</string> <string name="read_config">閱讀介面設定</string>
<string name="rule_image_style">圖片樣式(imageStyle)</string> <string name="rule_image_style">圖片樣式(imageStyle)</string>
<string name="rule_replace_regex">取代規則(replaceRegex)</string> <string name="rule_replace_regex">正文获取后处理规则(replaceRegex)</string>
<string name="group_name">分組名稱</string> <string name="group_name">分組名稱</string>
<string name="note_content">備註內容</string> <string name="note_content">備註內容</string>
<string name="replace_enable_default_t">預設啟用取代淨化</string> <string name="replace_enable_default_t">預設啟用取代淨化</string>

@ -418,8 +418,6 @@
<string name="rule_next_content">正文下一页URL规则(nextContentUrl)</string> <string name="rule_next_content">正文下一页URL规则(nextContentUrl)</string>
<string name="rule_web_js">webJs</string> <string name="rule_web_js">webJs</string>
<string name="rule_source_regex">资源正则(sourceRegex)</string> <string name="rule_source_regex">资源正则(sourceRegex)</string>
<string name="rule_font">网页内嵌字体(font)</string>
<string name="rule_correct_font">正确字体(font)</string>
<string name="source_icon">图标(sourceIcon)</string> <string name="source_icon">图标(sourceIcon)</string>
<string name="r_articles">列表规则(ruleArticles)</string> <string name="r_articles">列表规则(ruleArticles)</string>
@ -737,7 +735,7 @@
<string name="restore_ignore_summary">恢复时忽略一些内容不恢复,方便不同手机配置不同</string> <string name="restore_ignore_summary">恢复时忽略一些内容不恢复,方便不同手机配置不同</string>
<string name="read_config">阅读界面设置</string> <string name="read_config">阅读界面设置</string>
<string name="rule_image_style">图片样式(imageStyle)</string> <string name="rule_image_style">图片样式(imageStyle)</string>
<string name="rule_replace_regex">替换规则(replaceRegex)</string> <string name="rule_replace_regex">正文获取后处理规则(replaceRegex)</string>
<string name="group_name">分组名称</string> <string name="group_name">分组名称</string>
<string name="note_content">备注内容</string> <string name="note_content">备注内容</string>
<string name="replace_enable_default_t">默认启用替换净化</string> <string name="replace_enable_default_t">默认启用替换净化</string>

@ -5,4 +5,7 @@
<item name="tag2" type="id" /> <item name="tag2" type="id" />
<item name="fast_scroller" type="id" /> <item name="fast_scroller" type="id" />
<item name="bettermovementmethod_highlight_background_span" type="id" />
</resources> </resources>

@ -420,10 +420,8 @@
<string name="rule_next_content">正文下一页URL规则(nextContentUrl)</string> <string name="rule_next_content">正文下一页URL规则(nextContentUrl)</string>
<string name="rule_web_js">webJs</string> <string name="rule_web_js">webJs</string>
<string name="rule_source_regex">资源正则(sourceRegex)</string> <string name="rule_source_regex">资源正则(sourceRegex)</string>
<string name="rule_replace_regex">替换规则(replaceRegex)</string> <string name="rule_replace_regex">正文获取后处理规则(replaceRegex)</string>
<string name="rule_image_style">图片样式(imageStyle)</string> <string name="rule_image_style">图片样式(imageStyle)</string>
<string name="rule_font">网页内嵌字体(font)</string>
<string name="rule_correct_font">正确字体(font)</string>
<string name="source_icon">图标(sourceIcon)</string> <string name="source_icon">图标(sourceIcon)</string>
<string name="r_articles">列表规则(ruleArticles)</string> <string name="r_articles">列表规则(ruleArticles)</string>

Loading…
Cancel
Save