diff --git a/README.md b/README.md index 1c7bf34df..7c014360e 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,7 @@ ## 阅读3.0 书源规则 https://celeter.github.io +[![aVElJs.th.jpg](https://s1.ax1x.com/2020/07/28/aVElJs.th.jpg)](https://imgchr.com/i/aVElJs) [![aVEQij.th.jpg](https://s1.ax1x.com/2020/07/28/aVEQij.th.jpg)](https://imgchr.com/i/aVEQij) [![aVEudg.th.jpg](https://s1.ax1x.com/2020/07/28/aVEudg.th.jpg)](https://imgchr.com/i/aVEudg) [![aVEneS.th.jpg](https://s1.ax1x.com/2020/07/28/aVEneS.th.jpg)](https://imgchr.com/i/aVEneS) [![aVEKoQ.th.jpg](https://s1.ax1x.com/2020/07/28/aVEKoQ.th.jpg)](https://imgchr.com/i/aVEKoQ) [![aVE1Wn.th.jpg](https://s1.ax1x.com/2020/07/28/aVE1Wn.th.jpg)](https://imgchr.com/i/aVE1Wn) + ## 免责声明 https://gedoor.github.io/MyBookshelf/disclaimer.html diff --git a/app/build.gradle b/app/build.gradle index 01ca59663..26acce2d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -124,7 +124,7 @@ dependencies { implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' //androidX - implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.media:media:1.1.0' implementation 'androidx.preference:preference:1.1.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 03a8398b5..db69e078b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -295,10 +295,12 @@ android:launchMode="singleTop"> + - - + + + diff --git a/app/src/main/assets/18PlusList.txt b/app/src/main/assets/18PlusList.txt index dd800e91b..52a5bb3cc 100644 --- a/app/src/main/assets/18PlusList.txt +++ b/app/src/main/assets/18PlusList.txt @@ -1,3 +1,82 @@ OGN5dS5jb20= c2cwMC54eXo= -aXRyYWZmaWNuZXQuY29t \ No newline at end of file +aXRyYWZmaWNuZXQuY29t +eGlhb3FpYW5nNTIw +MTIzeGlhb3FpYW5n +eGlhb3FpYW5neHM= +eGlhb3FpYW5nNTIw +MzM1eHM= +eGN4czk= +eGN4czUyMA== +c2h1YmFvYW4= +c2h1YmFvd2FuZzEyMw== +c2h1YmFvYW4= +aGFpdGFuZzEyMw== +eXV6aGFpd3VsYQ== +cG8xOA== +Ymwtbm92ZWw= +NXRucw== +c2hhb3NodWdl +amluamlzaHV3dQ== +NDJ3Zw== +eWlxdXNodQ== +c2h1YmFvd2FuZzEyMw== +M2hlYmFv +MzNoZWJhbw== +bHVvcWl1enc= +bXlzaHVnZQ== +c3NzeHN3 +eWl0ZQ== +Y3Vpd2VpanV1 +Y3Vpd2VpanV4cw== +Y3Vpd2VpanV4 +eGlhb3FpYW5nd3g= +YXN6dw== +YXN6dzY= +c2FuaGFveHM= +ODdzaHV3dQ== +NDh3eA== +bG9uZ3Rlbmcy +NnF3eA== +bG9uZ3Rlbmd4cw== +aGF4ZHU= +M3R3eA== +aGF4d3g1 +NjZsZXdlbg== +eGJhbnpodQ== +aGR5cA== +ZHliejk= +ZGl5aWJhbnpodTk= +ZGl5aWJhbnpodQ== +ZGl5aWJhbnpodTc= +YnoyMjI= +d29kZWFwaTAwMQ== +dGFuZ3poZWthbg== +YmF4aWFueHM= +eGlhb3NodW9zaGVuemhhbg== +ZGFtb2tl +emh3ZW5wZw== +eXV6aGFpZ2U= +d21wOA== +OXhpYW53ZW4= +bmFucmVudmlw +cmV5b28= +eWZ4aWFvc2h1b2U= +c2Fuaml1enc= +N3Fpbmc3 +cWR4aWFvc2h1bw== +Y2hpbmVzZXpq +MzlzaHViYW8= +a3l4czU= +NTZtcw== +bml1c2hh +bWt4czY= +MjIyMjJ4cw== +OTVkdXNodQ== +YmFuemh1MjI= +d3JsdHh0 +dHVkb3V0eHQ= +cm5neHM= +OTl3ZW5rdQ== +bGFvc2lqaXhz +ZnVzaHV6aGFpMQ== diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index d80cfd98d..d8ce22ea8 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -3,6 +3,24 @@ * 请关注公众号[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 * 新公众号[开源阅读]()已启用,[开源阅读软件]()备用 +**2020/07/30** +* 所有版本都加上书源屏蔽列表,有其它想法的可以自己编译软件,反正是开源的 + +**2020/07/29** +* 正文图片样式为FULL的自动为滚动模式 + +**2020/07/28** +* 长图正文规则添加图片样式FULL,可以滚动浏览了 + +**2020/07/26** +* 优化翻页,加快翻页速度 + +**2020/07/25** +* 正文规则添加多页合并后的替换规则,格式同样是##regex##replaceTo +* 正文图片添加长按缩放 +* 正文规则添加图片样式规则,可以设置为FULL +* 其它一些bug修复 + **2020/07/21** * 优化文字选择,不再缓存 * 添加忽略恢复列表,方便不同手机配置不同 diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt index df7a8dae3..a3c565652 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt @@ -9,5 +9,6 @@ data class ContentRule( var nextContentUrl: String? = null, var webJs: String? = null, var sourceRegex: String? = null, - var imageStyle: Int = 0 //0默认大小居中,1最大宽度 + var replaceRegex: String? = null, + var imageStyle: String? = null //默认大小居中,1最大宽度 ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/AppConfig.kt b/app/src/main/java/io/legado/app/help/AppConfig.kt index 43d4e8ac3..0c4851f61 100644 --- a/app/src/main/java/io/legado/app/help/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/AppConfig.kt @@ -137,5 +137,7 @@ object AppConfig { val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true) val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google" + + val isCoolApk: Boolean get() = App.INSTANCE.channel == "coolApk" } diff --git a/app/src/main/java/io/legado/app/help/SourceHelp.kt b/app/src/main/java/io/legado/app/help/SourceHelp.kt index bdb883b10..bed6c83a8 100644 --- a/app/src/main/java/io/legado/app/help/SourceHelp.kt +++ b/app/src/main/java/io/legado/app/help/SourceHelp.kt @@ -3,6 +3,7 @@ package io.legado.app.help import android.os.Handler import android.os.Looper import io.legado.app.App +import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.utils.EncoderUtils import io.legado.app.utils.NetworkUtils @@ -33,9 +34,20 @@ object SourceHelp { } } + fun insertBookSource(vararg bookSources: BookSource) { + bookSources.forEach { bookSource -> + if (is18Plus(bookSource.bookSourceUrl)) { + handler.post { + App.INSTANCE.toast("${bookSource.bookSourceName}是18+网址,禁止导入.") + } + } else { + App.db.bookSourceDao().insert(bookSource) + } + } + } + private fun is18Plus(url: String?): Boolean { url ?: return false - if (AppConfig.isGooglePlay) return false val baseUrl = NetworkUtils.getBaseUrl(url) baseUrl ?: return false try { diff --git a/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt b/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt index 4f5cbda46..a12816486 100644 --- a/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt +++ b/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt @@ -1,12 +1,14 @@ package io.legado.app.help.http import android.annotation.SuppressLint -import android.os.Build import android.os.Handler import android.os.Looper import android.os.Message import android.text.TextUtils -import android.webkit.* +import android.webkit.CookieManager +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient import io.legado.app.App import io.legado.app.constant.AppConst import org.apache.commons.text.StringEscapeUtils @@ -142,30 +144,6 @@ class AjaxWebView { handler.postDelayed(runnable, 1000) } - override fun onReceivedError( - view: WebView, - errorCode: Int, - description: String, - failingUrl: String - ) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - handler.obtainMessage(MSG_ERROR, Exception(description)) - .sendToTarget() - } - } - - override fun onReceivedError( - view: WebView, - request: WebResourceRequest, - error: WebResourceError - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - handler.obtainMessage( - MSG_ERROR, - Exception(error.description.toString()) - ).sendToTarget() - } - } } private class EvalJsRunnable( @@ -213,31 +191,6 @@ class AjaxWebView { } } - override fun onReceivedError( - view: WebView, - errorCode: Int, - description: String, - failingUrl: String - ) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - handler.obtainMessage(MSG_ERROR, Exception(description)) - .sendToTarget() - } - } - - override fun onReceivedError( - view: WebView, - request: WebResourceRequest, - error: WebResourceError - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - handler.obtainMessage( - MSG_ERROR, - Exception(error.description.toString()) - ).sendToTarget() - } - } - override fun onPageFinished(view: WebView, url: String) { params.setCookie(url) if (params.hasJavaScript()) { diff --git a/app/src/main/java/io/legado/app/lib/theme/ATH.kt b/app/src/main/java/io/legado/app/lib/theme/ATH.kt index 1b0bf723d..a7822b84b 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ATH.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ATH.kt @@ -5,6 +5,7 @@ import android.app.Activity import android.app.ActivityManager import android.content.Context import android.graphics.Color +import android.graphics.drawable.GradientDrawable import android.os.Build import android.view.View import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR @@ -17,6 +18,7 @@ import androidx.viewpager.widget.ViewPager import com.google.android.material.bottomnavigation.BottomNavigationView import io.legado.app.R import io.legado.app.help.AppConfig +import io.legado.app.utils.dp import io.legado.app.utils.getCompatColor import kotlinx.android.synthetic.main.activity_main.view.* import org.jetbrains.anko.backgroundColor @@ -138,6 +140,10 @@ object ATH { } fun setAlertDialogTint(dialog: AlertDialog): AlertDialog { + val background = GradientDrawable() + background.cornerRadius = 3F.dp + background.setColor(dialog.context.backgroundColor) + dialog.window?.setBackgroundDrawable(background) val colorStateList = Selector.colorBuild() .setDefaultColor(ThemeStore.accentColor(dialog.context)) .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(dialog.context))) diff --git a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt index a5ee5b26a..9e5336b62 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt @@ -87,21 +87,27 @@ object BookContent { analyzeContent( book, item.nextUrl, it, contentRule, bookChapter, bookSource, false ) - item.content = contentData.content - } + item.content = contentData.content + } } } for (item in contentDataList) { content.append(item.content).append("\n") } } - content.deleteCharAt(content.length - 1) + var contentStr = content.toString().htmlFormat() + val replaceRegex = bookSource.ruleContent?.replaceRegex + replaceRegex?.trim { it <= ' ' }?.split("##")?.let { + if (it.size > 1) { + contentStr = contentStr.replace(it[1].toRegex(), it.getOrNull(2) ?: "") + } + } Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "└${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "┌获取正文内容") - Debug.log(bookSource.bookSourceUrl, "└\n$content") - return content.toString() + Debug.log(bookSource.bookSourceUrl, "└\n$contentStr") + return contentStr } @Throws(Exception::class) @@ -126,7 +132,7 @@ object BookContent { } Debug.log(bookSource.bookSourceUrl, "└" + nextUrlList.joinToString(","), printLog) } - val content = analyzeRule.getString(contentRule.content).htmlFormat() + val content = analyzeRule.getString(contentRule.content) return ContentData(content, nextUrlList) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index a7691b5b9..8f5c74f4e 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -64,6 +64,7 @@ object ReadBook { null } } + callBack?.upPageAnim() } fun upMsg(msg: String?) { @@ -313,7 +314,13 @@ object ReadBook { when (chapter.index) { durChapterIndex -> { curTextChapter = - ChapterProvider.getTextChapter(book, chapter, contents, chapterSize) + ChapterProvider.getTextChapter( + book, + chapter, + contents, + chapterSize, + imageStyle + ) if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) callBack?.upView() curPageChanged() @@ -322,12 +329,24 @@ object ReadBook { } durChapterIndex - 1 -> { prevTextChapter = - ChapterProvider.getTextChapter(book, chapter, contents, chapterSize) + ChapterProvider.getTextChapter( + book, + chapter, + contents, + chapterSize, + imageStyle + ) if (upContent) callBack?.upContent(-1, resetPageOffset) } durChapterIndex + 1 -> { nextTextChapter = - ChapterProvider.getTextChapter(book, chapter, contents, chapterSize) + ChapterProvider.getTextChapter( + book, + chapter, + contents, + chapterSize, + imageStyle + ) if (upContent) callBack?.upContent(1, resetPageOffset) } } @@ -338,6 +357,8 @@ object ReadBook { } } + val imageStyle get() = webBook?.bookSource?.ruleContent?.imageStyle + fun saveRead() { Coroutine.async { book?.let { book -> @@ -358,5 +379,6 @@ object ReadBook { fun upView() fun pageChanged() fun contentLoadFinish() + fun upPageAnim() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/Help.kt b/app/src/main/java/io/legado/app/ui/book/read/Help.kt index 853a070da..aa2d82b55 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/Help.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/Help.kt @@ -20,6 +20,7 @@ import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.backgroundColor import io.legado.app.service.help.Download import io.legado.app.service.help.ReadBook import io.legado.app.utils.applyTint @@ -131,6 +132,7 @@ object Help { LayoutInflater.from(context).inflate(R.layout.dialog_download_choice, null) .apply { view = this + setBackgroundColor(context.backgroundColor) edit_start.setText(book.durChapterIndex.toString()) edit_end.setText(book.totalChapterNum.toString()) } @@ -157,7 +159,7 @@ object Help { customView { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { editText = edit_view.apply { - hint = "备注内容" + setHint(R.string.note_content) } } } 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 38bd04b42..83c4cdf4d 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 @@ -550,6 +550,12 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } } + override fun upPageAnim() { + launch { + page_view?.upPageAnim() + } + } + /** * 页面改变 */ diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index e2365b047..95f81f96d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -16,6 +16,7 @@ import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ImageProvider +import io.legado.app.ui.widget.dialog.PhotoDialog import io.legado.app.utils.activity import io.legado.app.utils.getCompatColor import io.legado.app.utils.getPrefBoolean @@ -213,88 +214,70 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at ) { if (!selectAble) return if (!visibleRect.contains(x, y)) return - var relativeOffset = relativeOffset(0) - for ((lineIndex, textLine) in textPage.textLines.withIndex()) { - if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { - for ((charIndex, textChar) in textLine.textChars.withIndex()) { - if (x > textChar.start && x < textChar.end) { - textChar.selected = true - invalidate() - selectStart[0] = 0 - selectStart[1] = lineIndex - selectStart[2] = charIndex - selectEnd[0] = 0 - selectEnd[1] = lineIndex - selectEnd[2] = charIndex - upSelectedStart( - textChar.start, - textLine.lineBottom + relativeOffset, - textLine.lineTop + relativeOffset - ) - upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) - select(0, lineIndex, charIndex) - return - } - } - return + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!ReadBookConfig.isScroll) return + if (relativeOffset >= ChapterProvider.visibleHeight) return } - } - if (!ReadBookConfig.isScroll) return - //滚动翻页 - relativeOffset = relativeOffset(1) - if (relativeOffset >= ChapterProvider.visibleHeight) return - val nextPage = relativePage(1) - for ((lineIndex, textLine) in nextPage.textLines.withIndex()) { - if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { - for ((charIndex, textChar) in textLine.textChars.withIndex()) { - if (x > textChar.start && x < textChar.end) { - textChar.selected = true - invalidate() - selectStart[0] = 1 - selectStart[1] = lineIndex - selectStart[2] = charIndex - selectEnd[0] = 1 - selectEnd[1] = lineIndex - selectEnd[2] = charIndex - upSelectedStart( - textChar.start, - textLine.lineBottom + relativeOffset, - textLine.lineTop + relativeOffset - ) - upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) - select(1, lineIndex, charIndex) - return + val page = relativePage(relativePos) + for ((lineIndex, textLine) in page.textLines.withIndex()) { + if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + if (x > textChar.start && x < textChar.end) { + initSelect( + page.chapterIndex, + relativePos, + textLine, + textChar, + lineIndex, + charIndex, + relativeOffset, + select + ) + return + } } + return } - return } + } - relativeOffset = relativeOffset(2) - if (relativeOffset >= ChapterProvider.visibleHeight) return - for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) { - if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { - for ((charIndex, textChar) in textLine.textChars.withIndex()) { - if (x > textChar.start && x < textChar.end) { - textChar.selected = true - invalidate() - selectStart[0] = 2 - selectStart[1] = lineIndex - selectStart[2] = charIndex - selectEnd[0] = 2 - selectEnd[1] = lineIndex - selectEnd[2] = charIndex - upSelectedStart( - textChar.start, - textLine.lineBottom + relativeOffset, - textLine.lineTop + relativeOffset - ) - upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) - select(2, lineIndex, charIndex) - return - } - } - return + + } + + private fun initSelect( + chapterIndex: Int, + relativePage: Int, + textLine: TextLine, + textChar: TextChar, + lineIndex: Int, + charIndex: Int, + relativeOffset: Float, + select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit + ) { + if (textChar.isImage) { + activity?.supportFragmentManager?.let { + PhotoDialog.show(it, chapterIndex, textChar.charData) } + } else { + textChar.selected = true + invalidate() + selectStart[0] = relativePage + selectStart[1] = lineIndex + selectStart[2] = charIndex + selectEnd[0] = relativePage + selectEnd[1] = lineIndex + selectEnd[2] = charIndex + upSelectedStart( + textChar.start, + textLine.lineBottom + relativeOffset, + textLine.lineTop + relativeOffset + ) + upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) + select(relativePage, lineIndex, charIndex) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt index efad055aa..63e90d0fe 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt @@ -17,6 +17,7 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.utils.activity import io.legado.app.utils.screenshot import kotlinx.android.synthetic.main.activity_book_read.view.* +import java.util.* class PageView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs), @@ -109,7 +110,9 @@ class PageView(context: Context, attrs: AttributeSet) : fun upPageAnim() { pageDelegate?.onDestroy() pageDelegate = null - pageDelegate = when (ReadBookConfig.pageAnim) { + pageDelegate = if (ReadBook.imageStyle?.toUpperCase(Locale.ROOT) == "FULL") { + ScrollPageDelegate(this) + } else when (ReadBookConfig.pageAnim) { 0 -> CoverPageDelegate(this) 1 -> SlidePageDelegate(this) 2 -> SimulationPageDelegate(this) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt index cf18cb1db..cacb9d87d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.read.page.delegate +import android.view.MotionEvent import io.legado.app.ui.book.read.page.PageView class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { @@ -11,4 +12,8 @@ class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) stopScroll() } + override fun onDoubleTap(e: MotionEvent): Boolean { + return onSingleTapUp(e) + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt index 1f46e1dfa..914420fe2 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -10,12 +10,12 @@ import android.view.animation.DecelerateInterpolator import android.widget.Scroller import androidx.annotation.CallSuper import com.google.android.material.snackbar.Snackbar +import io.legado.app.R import io.legado.app.help.AppConfig import io.legado.app.help.ReadBookConfig import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.PageView import kotlin.math.abs -import io.legado.app.R abstract class PageDelegate(protected val pageView: PageView) : GestureDetector.SimpleOnGestureListener() { @@ -24,6 +24,7 @@ abstract class PageDelegate(protected val pageView: PageView) : pageView.width * 0.66f, pageView.height * 0.66f ) protected val context: Context = pageView.context + private val animationSpeed = 300 //起始点 protected var startX: Float = 0f @@ -115,13 +116,12 @@ abstract class PageDelegate(protected val pageView: PageView) : } protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int) { - scroller.startScroll( - startX, - startY, - dx, - dy, - if (dx != 0) (abs(dx) * 0.3).toInt() else (abs(dy) * 0.3).toInt() - ) + val duration = if (dx != 0) { + (animationSpeed * abs(dx)) / viewWidth + } else { + (animationSpeed * abs(dy)) / viewHeight + } + scroller.startScroll(startX, startY, dx, dy, duration) isRunning = true isStarted = true pageView.invalidate() diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt index fadb9ff53..beb9e3673 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt @@ -22,6 +22,7 @@ data class TextPage( fun upLinesPosition() = ChapterProvider.apply { if (!ReadBookConfig.textBottomJustify) return@apply if (textLines.size <= 1) return@apply + if (textLines.last().isImage) return@apply if (visibleHeight - height >= with(textLines.last()) { lineBottom - lineTop }) return@apply val surplus = (visibleBottom - textLines.last().lineBottom) if (surplus == 0f) return@apply diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt index d74d3fefe..ccfe10564 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt @@ -17,6 +17,7 @@ import io.legado.app.ui.book.read.page.entities.TextChar import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.utils.* +import java.util.* import java.util.regex.Pattern @@ -51,7 +52,8 @@ object ChapterProvider { book: Book, bookChapter: BookChapter, contents: List, - chapterSize: Int + chapterSize: Int, + imageStyle: String? ): TextChapter { val textPages = arrayListOf() val pageLines = arrayListOf() @@ -69,7 +71,7 @@ object ChapterProvider { src?.let { durY = setTypeImage( - book, bookChapter, src, durY, textPages + book, bookChapter, src, durY, textPages, imageStyle ) } } else { @@ -116,26 +118,39 @@ object ChapterProvider { chapter: BookChapter, src: String, y: Float, - textPages: ArrayList + textPages: ArrayList, + imageStyle: String? ): Float { var durY = y ImageProvider.getImage(book, chapter.index, src)?.let { - var height = it.height - var width = it.width - if (it.width > visibleWidth) { - height = it.height * visibleWidth / it.width - width = - visibleWidth - } - if (height > visibleHeight) { - width = width * visibleHeight / height - height = - visibleHeight - } - if (durY + height > visibleHeight) { + if (durY > visibleHeight) { + textPages.last().height = durY textPages.add(TextPage()) durY = 0f } + var height = it.height + var width = it.width + when (imageStyle?.toUpperCase(Locale.ROOT)) { + "FULL" -> { + width = visibleWidth + height = it.height * visibleWidth / it.width + } + else -> { + if (it.width > visibleWidth) { + height = it.height * visibleWidth / it.width + width = visibleWidth + } + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + if (durY + height > visibleHeight) { + textPages.last().height = durY + textPages.add(TextPage()) + durY = 0f + } + } + } val textLine = TextLine(isImage = true) textLine.lineTop = durY durY += height diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt index 76d772edd..6be688518 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt @@ -10,10 +10,11 @@ import io.legado.app.utils.FileUtils import io.legado.app.utils.MD5Utils import io.legado.app.utils.externalFilesDir import java.io.FileOutputStream +import java.util.concurrent.ConcurrentHashMap object ImageProvider { - private val cache = hashMapOf>() + private val cache = ConcurrentHashMap>() @Synchronized fun getCache(chapterIndex: Int, src: String): Bitmap? { @@ -24,7 +25,7 @@ object ImageProvider { fun setCache(chapterIndex: Int, src: String, bitmap: Bitmap) { var indexCache = cache[chapterIndex] if (indexCache == null) { - indexCache = hashMapOf() + indexCache = ConcurrentHashMap() cache[chapterIndex] = indexCache } indexCache[src] = bitmap diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt index 71ee36149..d53c9a05c 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt @@ -233,6 +233,8 @@ class BookSourceEditActivity : add(EditEntity("nextContentUrl", cr?.nextContentUrl, R.string.rule_next_content)) add(EditEntity("webJs", cr?.webJs, R.string.rule_web_js)) add(EditEntity("sourceRegex", cr?.sourceRegex, R.string.rule_source_regex)) + add(EditEntity("replaceRegex", cr?.replaceRegex, R.string.rule_replace_regex)) + add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style)) } //发现 val er = source?.getExploreRule() @@ -332,6 +334,8 @@ class BookSourceEditActivity : "nextContentUrl" -> contentRule.nextContentUrl = it.value "webJs" -> contentRule.webJs = it.value "sourceRegex" -> contentRule.sourceRegex = it.value + "replaceRegex" -> contentRule.replaceRegex = it.value + "imageStyle" -> contentRule.imageStyle = it.value } } source.ruleSearch = searchRule diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index c0e255783..7e2a75d6e 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -8,6 +8,7 @@ import com.jayway.jsonpath.JsonPath import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.BookSource +import io.legado.app.help.SourceHelp import io.legado.app.help.http.HttpHelper import io.legado.app.help.storage.OldRule import io.legado.app.help.storage.Restore.jsonPath @@ -205,7 +206,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) } } else { OldRule.jsonToBookSource(text1)?.let { - App.db.bookSourceDao().insert(it) + SourceHelp.insertBookSource(it) count = 1 } } @@ -220,7 +221,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) bookSources.add(it) } } - App.db.bookSourceDao().insert(*bookSources.toTypedArray()) + SourceHelp.insertBookSource(*bookSources.toTypedArray()) "导入${bookSources.size}条" } text1.isAbsUrl() -> { @@ -251,7 +252,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) bookSources.add(source) } } - App.db.bookSourceDao().insert(*bookSources.toTypedArray()) + SourceHelp.insertBookSource(*bookSources.toTypedArray()) return bookSources.size } } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt index d4a466948..9ed695a01 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt @@ -89,7 +89,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { customView { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { editText = edit_view.apply { - hint = "分组名称" + setHint(R.string.group_name) } } } @@ -111,7 +111,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { customView { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { editText = edit_view.apply { - hint = "分组名称" + setHint(R.string.group_name) setText(group) } } diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt index 845b73ef2..ec9942eba 100644 --- a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -80,9 +80,7 @@ class OtherConfigFragment : PreferenceFragmentCompat(), } PreferKey.cleanCache -> { BookHelp.clearCache() - requireActivity().cacheDir.listFiles()?.forEach { - it.delete() - } + FileUtils.deleteFile(requireActivity().cacheDir.absolutePath) toast(R.string.clear_cache_success) } PreferKey.defaultCover -> { diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 186f54a92..a3268db42 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -81,11 +81,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) { fun postLoad() { execute { - FileUtils.getDirFile(context.cacheDir, "Fonts").let { - if (it.exists()) { - it.delete() - } - } + FileUtils.deleteFile(FileUtils.getPath(context.cacheDir, "Fonts")) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt new file mode 100644 index 000000000..74dfe23cd --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt @@ -0,0 +1,68 @@ +package io.legado.app.ui.widget.dialog + +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.service.help.ReadBook +import io.legado.app.ui.book.read.page.provider.ImageProvider +import kotlinx.android.synthetic.main.dialog_photo_view.* + + +class PhotoDialog : BaseDialogFragment() { + + companion object { + + fun show( + fragmentManager: FragmentManager, + chapterIndex: Int, + src: String + ) { + PhotoDialog().apply { + val bundle = Bundle() + bundle.putInt("chapterIndex", chapterIndex) + bundle.putString("src", src) + arguments = bundle + }.show(fragmentManager, "photoDialog") + } + + } + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + activity?.windowManager?.defaultDisplay?.getMetrics(dm) + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_photo_view, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + arguments?.let { + val chapterIndex = it.getInt("chapterIndex") + val src = it.getString("src") + ReadBook.book?.let { book -> + src?.let { + ImageProvider.getImage(book, chapterIndex, src)?.let { bitmap -> + photo_view.setImageBitmap(bitmap) + } + } + } + } + + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt b/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt new file mode 100644 index 000000000..abfc81989 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt @@ -0,0 +1,1267 @@ +package io.legado.app.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.PointF +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.* +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.ScaleGestureDetector.OnScaleGestureListener +import android.view.animation.DecelerateInterpolator +import android.view.animation.Interpolator +import android.widget.ImageView +import android.widget.OverScroller +import android.widget.Scroller +import io.legado.app.R +import io.legado.app.ui.widget.image.photo.Info +import io.legado.app.ui.widget.image.photo.OnRotateListener +import io.legado.app.ui.widget.image.photo.RotateGestureDetector +import kotlin.math.abs +import kotlin.math.roundToInt + + +@SuppressLint("AppCompatCustomView") +class PhotoView : ImageView { + val MIN_ROTATE = 35 + val ANIMA_DURING = 340 + val MAX_SCALE = 2.5f + + private var mMinRotate = 0 + var mAnimaDuring = 0 + private var mMaxScale = 0f + + var MAX_OVER_SCROLL = 0 + var MAX_FLING_OVER_SCROLL = 0 + var MAX_OVER_RESISTANCE = 0 + var MAX_ANIM_FROM_WAITE = 500 + + private val mBaseMatrix: Matrix = Matrix() + private val mAnimMatrix: Matrix = Matrix() + private val mSynthesisMatrix: Matrix = Matrix() + private val mTmpMatrix: Matrix = Matrix() + + private var mRotateDetector: RotateGestureDetector? = null + private var mDetector: GestureDetector? = null + private var mScaleDetector: ScaleGestureDetector? = null + private var mClickListener: OnClickListener? = null + + private var mScaleType: ScaleType? = null + + private var hasMultiTouch = false + private var hasDrawable = false + private var isKnowSize = false + private var hasOverTranslate = false + + //缩放 + var isEnable = true + + //旋转 + var isRotateEnable = false + private var isInit = false + private var mAdjustViewBounds = false + + // 当前是否处于放大状态 + private var isZoonUp = false + private var canRotate = false + + private var imgLargeWidth = false + private var imgLargeHeight = false + + private var mRotateFlag = 0f + private var mDegrees = 0f + private var mScale = 1.0f + private var mTranslateX = 0 + private var mTranslateY = 0 + + private var mHalfBaseRectWidth = 0f + private var mHalfBaseRectHeight = 0f + + private val mWidgetRect = RectF() + private val mBaseRect = RectF() + private val mImgRect = RectF() + private val mTmpRect = RectF() + private val mCommonRect = RectF() + + private val mScreenCenter = PointF() + private val mScaleCenter = PointF() + private val mRotateCenter = PointF() + + private val mTranslate: Transform = Transform() + + private var mClip: RectF? = null + private var mFromInfo: Info? = null + private var mInfoTime: Long = 0 + private var mCompleteCallBack: Runnable? = null + + private var mLongClick: OnLongClickListener? = null + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init() + } + + private fun init() { + super.setScaleType(ScaleType.MATRIX) + if (mScaleType == null) mScaleType = ScaleType.CENTER_INSIDE + mRotateDetector = RotateGestureDetector(mRotateListener) + mDetector = GestureDetector(context, mGestureListener) + mScaleDetector = ScaleGestureDetector(context, mScaleListener) + val density = resources.displayMetrics.density + MAX_OVER_SCROLL = (density * 30).toInt() + MAX_FLING_OVER_SCROLL = (density * 30).toInt() + MAX_OVER_RESISTANCE = (density * 140).toInt() + mMinRotate = MIN_ROTATE + mAnimaDuring = ANIMA_DURING + mMaxScale = MAX_SCALE + } + + /** + * 获取默认的动画持续时间 + */ + fun getDefaultAnimDuring(): Int { + return ANIMA_DURING + } + + override fun setOnClickListener(l: OnClickListener?) { + super.setOnClickListener(l) + mClickListener = l + } + + override fun setScaleType(scaleType: ScaleType) { + if (scaleType == ScaleType.MATRIX) return + if (scaleType != mScaleType) { + mScaleType = scaleType + if (isInit) { + initBase() + } + } + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + mLongClick = l + } + + /** + * 设置动画的插入器 + */ + fun setInterpolator(interpolator: Interpolator?) { + mTranslate.setInterpolator(interpolator) + } + + /** + * 获取动画持续时间 + */ + fun getAnimDuring(): Int { + return mAnimaDuring + } + + /** + * 设置动画的持续时间 + */ + fun setAnimDuring(during: Int) { + mAnimaDuring = during + } + + /** + * 设置最大可以缩放的倍数 + */ + fun setMaxScale(maxScale: Float) { + mMaxScale = maxScale + } + + /** + * 获取最大可以缩放的倍数 + */ + fun getMaxScale(): Float { + return mMaxScale + } + + /** + */ + fun setMaxAnimFromWaiteTime(wait: Int) { + MAX_ANIM_FROM_WAITE = wait + } + + override fun setImageResource(resId: Int) { + var drawable: Drawable? = null + try { + drawable = resources.getDrawable(resId, null) + } catch (e: Exception) { + } + setImageDrawable(drawable) + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + if (drawable == null) { + hasDrawable = false + return + } + if (!hasSize(drawable)) return + if (!hasDrawable) { + hasDrawable = true + } + initBase() + } + + private fun hasSize(d: Drawable): Boolean { + return !((d.intrinsicHeight <= 0 || d.intrinsicWidth <= 0) + && (d.minimumWidth <= 0 || d.minimumHeight <= 0) + && (d.bounds.width() <= 0 || d.bounds.height() <= 0)) + } + + private fun getDrawableWidth(d: Drawable): Int { + var width = d.intrinsicWidth + if (width <= 0) width = d.minimumWidth + if (width <= 0) width = d.bounds.width() + return width + } + + private fun getDrawableHeight(d: Drawable): Int { + var height = d.intrinsicHeight + if (height <= 0) height = d.minimumHeight + if (height <= 0) height = d.bounds.height() + return height + } + + private fun initBase() { + if (!hasDrawable) return + if (!isKnowSize) return + mBaseMatrix.reset() + mAnimMatrix.reset() + isZoonUp = false + val img = drawable + val w = width + val h = height + val imgw = getDrawableWidth(img) + val imgh = getDrawableHeight(img) + mBaseRect[0f, 0f, imgw.toFloat()] = imgh.toFloat() + + // 以图片中心点居中位移 + val tx = (w - imgw) / 2 + val ty = (h - imgh) / 2 + var sx = 1f + var sy = 1f + + // 缩放,默认不超过屏幕大小 + if (imgw > w) { + sx = w.toFloat() / imgw + } + if (imgh > h) { + sy = h.toFloat() / imgh + } + val scale = if (sx < sy) sx else sy + mBaseMatrix.reset() + mBaseMatrix.postTranslate(tx.toFloat(), ty.toFloat()) + mBaseMatrix.postScale(scale, scale, mScreenCenter.x, mScreenCenter.y) + mBaseMatrix.mapRect(mBaseRect) + mHalfBaseRectWidth = mBaseRect.width() / 2 + mHalfBaseRectHeight = mBaseRect.height() / 2 + mScaleCenter.set(mScreenCenter) + mRotateCenter.set(mScaleCenter) + executeTranslate() + when (mScaleType) { + ScaleType.CENTER -> initCenter() + ScaleType.CENTER_CROP -> initCenterCrop() + ScaleType.CENTER_INSIDE -> initCenterInside() + ScaleType.FIT_CENTER -> initFitCenter() + ScaleType.FIT_START -> initFitStart() + ScaleType.FIT_END -> initFitEnd() + ScaleType.FIT_XY -> initFitXY() + } + isInit = true + mFromInfo?.let { + if (System.currentTimeMillis() - mInfoTime < MAX_ANIM_FROM_WAITE) { + animaFrom(it) + } + } + mFromInfo = null + } + + private fun initCenter() { + if (!hasDrawable) return + if (!isKnowSize) return + val img = drawable + val imgw = getDrawableWidth(img) + val imgh = getDrawableHeight(img) + if (imgw > mWidgetRect.width() || imgh > mWidgetRect.height()) { + val scaleX = imgw / mImgRect.width() + val scaleY = imgh / mImgRect.height() + mScale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initCenterCrop() { + if (mImgRect.width() < mWidgetRect.width() || mImgRect.height() < mWidgetRect.height()) { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mScale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initCenterInside() { + if (mImgRect.width() > mWidgetRect.width() || mImgRect.height() > mWidgetRect.height()) { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mScale = if (scaleX < scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initFitCenter() { + if (mImgRect.width() < mWidgetRect.width()) { + mScale = mWidgetRect.width() / mImgRect.width() + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initFitStart() { + initFitCenter() + val ty = -mImgRect.top + mAnimMatrix.postTranslate(0f, ty) + executeTranslate() + resetBase() + mTranslateY += ty.toInt() + } + + private fun initFitEnd() { + initFitCenter() + val ty = mWidgetRect.bottom - mImgRect.bottom + mTranslateY += ty.toInt() + mAnimMatrix.postTranslate(0f, ty) + executeTranslate() + resetBase() + } + + private fun initFitXY() { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mAnimMatrix.postScale(scaleX, scaleY, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + + private fun resetBase() { + val img = drawable + val imgw = getDrawableWidth(img) + val imgh = getDrawableHeight(img) + mBaseRect[0f, 0f, imgw.toFloat()] = imgh.toFloat() + mBaseMatrix.set(mSynthesisMatrix) + mBaseMatrix.mapRect(mBaseRect) + mHalfBaseRectWidth = mBaseRect.width() / 2 + mHalfBaseRectHeight = mBaseRect.height() / 2 + mScale = 1f + mTranslateX = 0 + mTranslateY = 0 + mAnimMatrix.reset() + } + + private fun executeTranslate() { + mSynthesisMatrix.set(mBaseMatrix) + mSynthesisMatrix.postConcat(mAnimMatrix) + imageMatrix = mSynthesisMatrix + mAnimMatrix.mapRect(mImgRect, mBaseRect) + imgLargeWidth = mImgRect.width() > mWidgetRect.width() + imgLargeHeight = mImgRect.height() > mWidgetRect.height() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (!hasDrawable) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + val d = drawable + val drawableW = getDrawableWidth(d) + val drawableH = getDrawableHeight(d) + val pWidth = MeasureSpec.getSize(widthMeasureSpec) + val pHeight = MeasureSpec.getSize(heightMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + var width: Int + var height: Int + var p = layoutParams + if (p == null) { + p = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + width = if (p.width == ViewGroup.LayoutParams.MATCH_PARENT) { + if (widthMode == MeasureSpec.UNSPECIFIED) { + drawableW + } else { + pWidth + } + } else { + if (widthMode == MeasureSpec.EXACTLY) { + pWidth + } else if (widthMode == MeasureSpec.AT_MOST) { + if (drawableW > pWidth) pWidth else drawableW + } else { + drawableW + } + } + height = if (p.height == ViewGroup.LayoutParams.MATCH_PARENT) { + if (heightMode == MeasureSpec.UNSPECIFIED) { + drawableH + } else { + pHeight + } + } else { + if (heightMode == MeasureSpec.EXACTLY) { + pHeight + } else if (heightMode == MeasureSpec.AT_MOST) { + if (drawableH > pHeight) pHeight else drawableH + } else { + drawableH + } + } + if (mAdjustViewBounds && drawableW.toFloat() / drawableH != width.toFloat() / height) { + val hScale = height.toFloat() / drawableH + val wScale = width.toFloat() / drawableW + val scale = if (hScale < wScale) hScale else wScale + width = + if (p.width == ViewGroup.LayoutParams.MATCH_PARENT) width else (drawableW * scale).toInt() + height = + if (p.height == ViewGroup.LayoutParams.MATCH_PARENT) height else (drawableH * scale).toInt() + } + setMeasuredDimension(width, height) + } + + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + super.setAdjustViewBounds(adjustViewBounds) + mAdjustViewBounds = adjustViewBounds + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mWidgetRect[0f, 0f, w.toFloat()] = h.toFloat() + mScreenCenter[w / 2.toFloat()] = h / 2.toFloat() + if (!isKnowSize) { + isKnowSize = true + initBase() + } + } + + override fun draw(canvas: Canvas) { + mClip?.let { + canvas.clipRect(it) + mClip = null + } + super.draw(canvas) + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return if (isEnable) { + val action = event.actionMasked + if (event.pointerCount >= 2) hasMultiTouch = true + mDetector!!.onTouchEvent(event) + if (isRotateEnable) { + mRotateDetector!!.onTouchEvent(event) + } + mScaleDetector!!.onTouchEvent(event) + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) onUp() + true + } else { + super.dispatchTouchEvent(event) + } + } + + private fun onUp() { + if (mTranslate.isRunning) return + if (canRotate || mDegrees % 90 != 0f) { + var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() + val remainder = mDegrees % 90 + if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f + mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) + mDegrees = toDegrees + } + var scale = mScale + if (mScale < 1) { + scale = 1f + mTranslate.withScale(mScale, 1F) + } else if (mScale > mMaxScale) { + scale = mMaxScale + mTranslate.withScale(mScale, mMaxScale) + } + val cx = mImgRect.left + mImgRect.width() / 2 + val cy = mImgRect.top + mImgRect.height() / 2 + mScaleCenter[cx] = cy + mRotateCenter[cx] = cy + mTranslateX = 0 + mTranslateY = 0 + mTmpMatrix.reset() + mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mTmpMatrix.postTranslate(cx - mHalfBaseRectWidth, cy - mHalfBaseRectHeight) + mTmpMatrix.postScale(scale, scale, cx, cy) + mTmpMatrix.postRotate(mDegrees, cx, cy) + mTmpMatrix.mapRect(mTmpRect, mBaseRect) + doTranslateReset(mTmpRect) + mTranslate.start() + } + + private fun doTranslateReset(imgRect: RectF) { + var tx = 0 + var ty = 0 + if (imgRect.width() <= mWidgetRect.width()) { + if (!isImageCenterWidth(imgRect)) tx = + (-((mWidgetRect.width() - imgRect.width()) / 2 - imgRect.left)).toInt() + } else { + if (imgRect.left > mWidgetRect.left) { + tx = (imgRect.left - mWidgetRect.left).toInt() + } else if (imgRect.right < mWidgetRect.right) { + tx = (imgRect.right - mWidgetRect.right).toInt() + } + } + if (imgRect.height() <= mWidgetRect.height()) { + if (!isImageCenterHeight(imgRect)) ty = + (-((mWidgetRect.height() - imgRect.height()) / 2 - imgRect.top)).toInt() + } else { + if (imgRect.top > mWidgetRect.top) { + ty = (imgRect.top - mWidgetRect.top).toInt() + } else if (imgRect.bottom < mWidgetRect.bottom) { + ty = (imgRect.bottom - mWidgetRect.bottom).toInt() + } + } + if (tx != 0 || ty != 0) { + if (!mTranslate.mFlingScroller.isFinished) mTranslate.mFlingScroller.abortAnimation() + mTranslate.withTranslate(mTranslateX, mTranslateY, -tx, -ty) + } + } + + private fun isImageCenterHeight(rect: RectF): Boolean { + return abs(rect.top.roundToInt() - (mWidgetRect.height() - rect.height()) / 2) < 1 + } + + private fun isImageCenterWidth(rect: RectF): Boolean { + return abs(rect.left.roundToInt() - (mWidgetRect.width() - rect.width()) / 2) < 1 + } + + private val mRotateListener: OnRotateListener = object : + OnRotateListener { + override fun onRotate( + degrees: Float, + focusX: Float, + focusY: Float + ) { + mRotateFlag += degrees + if (canRotate) { + mDegrees += degrees + mAnimMatrix.postRotate(degrees, focusX, focusY) + } else { + if (abs(mRotateFlag) >= mMinRotate) { + canRotate = true + mRotateFlag = 0f + } + } + } + } + + private val mScaleListener: OnScaleGestureListener = object : OnScaleGestureListener { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scaleFactor = detector.scaleFactor + if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false + mScale *= scaleFactor + //mScaleCenter.set(detector.getFocusX(), detector.getFocusY()); + mAnimMatrix.postScale( + scaleFactor, + scaleFactor, + detector.focusX, + detector.focusY + ) + executeTranslate() + return true + } + + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) {} + } + + private fun resistanceScrollByX( + overScroll: Float, + detalX: Float + ): Float { + return detalX * (abs(abs(overScroll) - MAX_OVER_RESISTANCE) / MAX_OVER_RESISTANCE.toFloat()) + } + + private fun resistanceScrollByY( + overScroll: Float, + detalY: Float + ): Float { + return detalY * (abs(abs(overScroll) - MAX_OVER_RESISTANCE) / MAX_OVER_RESISTANCE.toFloat()) + } + + /** + * 匹配两个Rect的共同部分输出到out,若无共同部分则输出0,0,0,0 + */ + private fun mapRect(r1: RectF, r2: RectF, out: RectF) { + val l: Float = if (r1.left > r2.left) r1.left else r2.left + val r: Float = if (r1.right < r2.right) r1.right else r2.right + if (l > r) { + out[0f, 0f, 0f] = 0f + return + } + val t: Float = if (r1.top > r2.top) r1.top else r2.top + val b: Float = if (r1.bottom < r2.bottom) r1.bottom else r2.bottom + if (t > b) { + out[0f, 0f, 0f] = 0f + return + } + out[l, t, r] = b + } + + private fun checkRect() { + if (!hasOverTranslate) { + mapRect(mWidgetRect, mImgRect, mCommonRect) + } + } + + private val mClickRunnable = Runnable { + mClickListener?.onClick(this) + } + + private val mGestureListener: GestureDetector.OnGestureListener = + object : SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + mLongClick?.onLongClick(this@PhotoView) + } + + override fun onDown(e: MotionEvent): Boolean { + hasOverTranslate = false + hasMultiTouch = false + canRotate = false + removeCallbacks(mClickRunnable) + return false + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (hasMultiTouch) return false + if (!imgLargeWidth && !imgLargeHeight) return false + if (mTranslate.isRunning) return false + var vx = velocityX + var vy = velocityY + if (mImgRect.left.roundToInt() >= mWidgetRect.left + || mImgRect.right.roundToInt() <= mWidgetRect.right + ) { + vx = 0f + } + if (mImgRect.top.roundToInt() >= mWidgetRect.top + || mImgRect.bottom.roundToInt() <= mWidgetRect.bottom + ) { + vy = 0f + } + if (canRotate || mDegrees % 90 != 0f) { + var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() + val remainder = mDegrees % 90 + if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f + mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) + mDegrees = toDegrees + } + doTranslateReset(mImgRect) + mTranslate.withFling(vx, vy) + mTranslate.start() + // onUp(e2); + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + var x = distanceX + var y = distanceY + if (mTranslate.isRunning) { + mTranslate.stop() + } + if (canScrollHorizontallySelf(x)) { + if (x < 0 && mImgRect.left - x > mWidgetRect.left) + x = mImgRect.left + if (x > 0 && mImgRect.right - x < mWidgetRect.right) + x = mImgRect.right - mWidgetRect.right + mAnimMatrix.postTranslate(-x, 0f) + mTranslateX -= x.toInt() + } else if (imgLargeWidth || hasMultiTouch || hasOverTranslate) { + checkRect() + if (!hasMultiTouch) { + if (x < 0 && mImgRect.left - x > mCommonRect.left) x = + resistanceScrollByX(mImgRect.left - mCommonRect.left, x) + if (x > 0 && mImgRect.right - x < mCommonRect.right) x = + resistanceScrollByX(mImgRect.right - mCommonRect.right, x) + } + mTranslateX -= x.toInt() + mAnimMatrix.postTranslate(-x, 0f) + hasOverTranslate = true + } + if (canScrollVerticallySelf(y)) { + if (y < 0 && mImgRect.top - y > mWidgetRect.top) y = + mImgRect.top + if (y > 0 && mImgRect.bottom - y < mWidgetRect.bottom) y = + mImgRect.bottom - mWidgetRect.bottom + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + } else if (imgLargeHeight || hasOverTranslate || hasMultiTouch) { + checkRect() + if (!hasMultiTouch) { + if (y < 0 && mImgRect.top - y > mCommonRect.top) y = + resistanceScrollByY(mImgRect.top - mCommonRect.top, y) + if (y > 0 && mImgRect.bottom - y < mCommonRect.bottom) y = + resistanceScrollByY(mImgRect.bottom - mCommonRect.bottom, y) + } + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + hasOverTranslate = true + } + executeTranslate() + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + postDelayed(mClickRunnable, 250) + return false + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + mTranslate.stop() + val from: Float + val to: Float + val imgCx = mImgRect.left + mImgRect.width() / 2 + val imgCy = mImgRect.top + mImgRect.height() / 2 + mScaleCenter[imgCx] = imgCy + mRotateCenter[imgCx] = imgCy + mTranslateX = 0 + mTranslateY = 0 + if (isZoonUp) { + from = mScale + to = 1f + } else { + from = mScale + to = mMaxScale + mScaleCenter[e.x] = e.y + } + mTmpMatrix.reset() + mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mTmpMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) + mTmpMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postScale(to, to, mScaleCenter.x, mScaleCenter.y) + mTmpMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) + mTmpMatrix.mapRect(mTmpRect, mBaseRect) + doTranslateReset(mTmpRect) + isZoonUp = !isZoonUp + mTranslate.withScale(from, to) + mTranslate.start() + return false + } + } + + fun canScrollHorizontallySelf(direction: Float): Boolean { + if (mImgRect.width() <= mWidgetRect.width()) + return false + if (direction < 0 && mImgRect.left.roundToInt() - direction >= mWidgetRect.left) + return false + return !(direction > 0 && mImgRect.right.roundToInt() - direction <= mWidgetRect.right) + } + + fun canScrollVerticallySelf(direction: Float): Boolean { + if (mImgRect.height() <= mWidgetRect.height()) + return false + if (direction < 0 && mImgRect.top.roundToInt() - direction >= mWidgetRect.top) + return false + return !(direction > 0 && mImgRect.bottom.roundToInt() - direction <= mWidgetRect.bottom) + } + + override fun canScrollHorizontally(direction: Int): Boolean { + return if (hasMultiTouch) true else canScrollHorizontallySelf(direction.toFloat()) + } + + override fun canScrollVertically(direction: Int): Boolean { + return if (hasMultiTouch) true else canScrollVerticallySelf(direction.toFloat()) + } + + private inner class InterpolatorProxy : Interpolator { + private var mTarget: Interpolator? + + init { + mTarget = DecelerateInterpolator() + } + + fun setTargetInterpolator(interpolator: Interpolator?) { + mTarget = interpolator + } + + override fun getInterpolation(input: Float): Float { + return mTarget?.getInterpolation(input) ?: input + } + + } + + private inner class Transform internal constructor() : Runnable { + var isRunning = false + var mTranslateScroller: OverScroller + var mFlingScroller: OverScroller + var mScaleScroller: Scroller + var mClipScroller: Scroller + var mRotateScroller: Scroller + var c: ClipCalculate? = null + var mLastFlingX = 0 + var mLastFlingY = 0 + var mLastTranslateX = 0 + var mLastTranslateY = 0 + var mClipRect = RectF() + var mInterpolatorProxy = InterpolatorProxy() + + fun setInterpolator(interpolator: Interpolator?) { + mInterpolatorProxy.setTargetInterpolator(interpolator) + } + + init { + val ctx: Context = context + mTranslateScroller = OverScroller(ctx, mInterpolatorProxy) + mScaleScroller = Scroller(ctx, mInterpolatorProxy) + mFlingScroller = OverScroller(ctx, mInterpolatorProxy) + mClipScroller = Scroller(ctx, mInterpolatorProxy) + mRotateScroller = Scroller(ctx, mInterpolatorProxy) + } + + fun withTranslate(startX: Int, startY: Int, deltaX: Int, deltaY: Int) { + mLastTranslateX = 0 + mLastTranslateY = 0 + mTranslateScroller.startScroll(0, 0, deltaX, deltaY, mAnimaDuring) + } + + fun withScale(form: Float, to: Float) { + mScaleScroller.startScroll( + (form * 10000).toInt(), + 0, + ((to - form) * 10000).toInt(), + 0, + mAnimaDuring + ) + } + + fun withClip( + fromX: Float, + fromY: Float, + deltaX: Float, + deltaY: Float, + d: Int, + c: ClipCalculate? + ) { + mClipScroller.startScroll( + (fromX * 10000).toInt(), + (fromY * 10000).toInt(), + (deltaX * 10000).toInt(), + (deltaY * 10000).toInt(), + d + ) + this.c = c + } + + fun withRotate(fromDegrees: Int, toDegrees: Int) { + mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, mAnimaDuring) + } + + fun withRotate(fromDegrees: Int, toDegrees: Int, during: Int) { + mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, during) + } + + fun withFling(velocityX: Float, velocityY: Float) { + mLastFlingX = if (velocityX < 0) Int.MAX_VALUE else 0 + var distanceX = + (if (velocityX > 0) abs(mImgRect.left) else mImgRect.right - mWidgetRect.right).toInt() + distanceX = if (velocityX < 0) Int.MAX_VALUE - distanceX else distanceX + var minX = if (velocityX < 0) distanceX else 0 + var maxX = if (velocityX < 0) Int.MAX_VALUE else distanceX + val overX = if (velocityX < 0) Int.MAX_VALUE - minX else distanceX + mLastFlingY = if (velocityY < 0) Int.MAX_VALUE else 0 + var distanceY = + (if (velocityY > 0) abs(mImgRect.top) else mImgRect.bottom - mWidgetRect.bottom).toInt() + distanceY = if (velocityY < 0) Int.MAX_VALUE - distanceY else distanceY + var minY = if (velocityY < 0) distanceY else 0 + var maxY = if (velocityY < 0) Int.MAX_VALUE else distanceY + val overY = if (velocityY < 0) Int.MAX_VALUE - minY else distanceY + if (velocityX == 0f) { + maxX = 0 + minX = 0 + } + if (velocityY == 0f) { + maxY = 0 + minY = 0 + } + mFlingScroller.fling( + mLastFlingX, + mLastFlingY, + velocityX.toInt(), + velocityY.toInt(), + minX, + maxX, + minY, + maxY, + if (abs(overX) < MAX_FLING_OVER_SCROLL * 2) 0 else MAX_FLING_OVER_SCROLL, + if (abs(overY) < MAX_FLING_OVER_SCROLL * 2) 0 else MAX_FLING_OVER_SCROLL + ) + } + + fun start() { + isRunning = true + postExecute() + } + + fun stop() { + removeCallbacks(this) + mTranslateScroller.abortAnimation() + mScaleScroller.abortAnimation() + mFlingScroller.abortAnimation() + mRotateScroller.abortAnimation() + isRunning = false + } + + override fun run() { + + // if (!isRuning) return; + var endAnima = true + if (mScaleScroller.computeScrollOffset()) { + mScale = mScaleScroller.currX / 10000f + endAnima = false + } + if (mTranslateScroller.computeScrollOffset()) { + val tx = mTranslateScroller.currX - mLastTranslateX + val ty = mTranslateScroller.currY - mLastTranslateY + mTranslateX += tx + mTranslateY += ty + mLastTranslateX = mTranslateScroller.currX + mLastTranslateY = mTranslateScroller.currY + endAnima = false + } + if (mFlingScroller.computeScrollOffset()) { + val x = mFlingScroller.currX - mLastFlingX + val y = mFlingScroller.currY - mLastFlingY + mLastFlingX = mFlingScroller.currX + mLastFlingY = mFlingScroller.currY + mTranslateX += x + mTranslateY += y + endAnima = false + } + if (mRotateScroller.computeScrollOffset()) { + mDegrees = mRotateScroller.currX.toFloat() + endAnima = false + } + if (mClipScroller.computeScrollOffset() || mClip != null) { + val sx = mClipScroller.currX / 10000f + val sy = mClipScroller.currY / 10000f + mTmpMatrix.setScale( + sx, + sy, + (mImgRect.left + mImgRect.right) / 2, + c!!.calculateTop() + ) + mTmpMatrix.mapRect(mClipRect, mImgRect) + if (sx == 1f) { + mClipRect.left = mWidgetRect.left + mClipRect.right = mWidgetRect.right + } + if (sy == 1f) { + mClipRect.top = mWidgetRect.top + mClipRect.bottom = mWidgetRect.bottom + } + mClip = mClipRect + } + if (!endAnima) { + applyAnima() + postExecute() + } else { + isRunning = false + + // 修复动画结束后边距有些空隙, + var needFix = false + if (imgLargeWidth) { + if (mImgRect.left > 0) { + mTranslateX -= mImgRect.left.toInt() + } else if (mImgRect.right < mWidgetRect.width()) { + mTranslateX -= (mWidgetRect.width() - mImgRect.right).toInt() + } + needFix = true + } + if (imgLargeHeight) { + if (mImgRect.top > 0) { + mTranslateY -= mImgRect.top.toInt() + } else if (mImgRect.bottom < mWidgetRect.height()) { + mTranslateY -= (mWidgetRect.height() - mImgRect.bottom).toInt() + } + needFix = true + } + if (needFix) { + applyAnima() + } + invalidate() + mCompleteCallBack?.let { + it.run() + mCompleteCallBack = null + } + } + } + + private fun applyAnima() { + mAnimMatrix.reset() + mAnimMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mAnimMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) + mAnimMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) + mAnimMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) + mAnimMatrix.postScale(mScale, mScale, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) + executeTranslate() + } + + private fun postExecute() { + if (isRunning) post(this) + } + + } + + fun getInfo(): Info { + val rect = RectF() + val p = IntArray(2) + getLocation(this, p) + rect[p[0] + mImgRect.left, p[1] + mImgRect.top, p[0] + mImgRect.right] = + p[1] + mImgRect.bottom + return Info( + rect, + mImgRect, + mWidgetRect, + mBaseRect, + mScreenCenter, + mScale, + mDegrees, + mScaleType + ) + } + + fun getImageViewInfo(imgView: ImageView): Info? { + val p = IntArray(2) + getLocation(imgView, p) + val drawable: Drawable = imgView.drawable + val matrix: Matrix = imgView.imageMatrix + val width = getDrawableWidth(drawable) + val height = getDrawableHeight(drawable) + val imgRect = RectF(0F, 0F, width.toFloat(), height.toFloat()) + matrix.mapRect(imgRect) + val rect = RectF( + p[0] + imgRect.left, + p[1] + imgRect.top, + p[0] + imgRect.right, + p[1] + imgRect.bottom + ) + val widgetRect = RectF(0F, 0F, imgView.width.toFloat(), imgView.height.toFloat()) + val baseRect = RectF(widgetRect) + val screenCenter = PointF(widgetRect.width() / 2, widgetRect.height() / 2) + return Info( + rect, + imgRect, + widgetRect, + baseRect, + screenCenter, + 1F, + 0F, + imgView.scaleType + ) + } + + private fun getLocation(target: View, position: IntArray) { + position[0] += target.left + position[1] += target.top + var viewParent: ViewParent = target.parent + while (viewParent is View) { + val view: View = viewParent + if (view.id == R.id.content) return + position[0] -= view.scrollX + position[1] -= view.scrollY + position[0] += view.left + position[1] += view.top + viewParent = view.parent + } + position[0] = (position[0] + 0.5f).toInt() + position[1] = (position[1] + 0.5f).toInt() + } + + private fun reset() { + mAnimMatrix.reset() + executeTranslate() + mScale = 1f + mTranslateX = 0 + mTranslateY = 0 + } + + interface ClipCalculate { + fun calculateTop(): Float + } + + inner class START : ClipCalculate { + override fun calculateTop(): Float { + return mImgRect.top + } + } + + inner class END : ClipCalculate { + override fun calculateTop(): Float { + return mImgRect.bottom + } + } + + inner class OTHER : ClipCalculate { + override fun calculateTop(): Float { + return (mImgRect.top + mImgRect.bottom) / 2 + } + } + + /** + * 在PhotoView内部还没有图片的时候同样可以调用该方法 + * + * + * 此时并不会播放动画,当给PhotoView设置图片后会自动播放动画。 + * + * + * 若等待时间过长也没有给控件设置图片,则会忽略该动画,若要再次播放动画则需要重新调用该方法 + * (等待的时间默认500毫秒,可以通过setMaxAnimFromWaiteTime(int)设置最大等待时间) + */ + fun animaFrom(info: Info) { + if (isInit) { + reset() + val mine = getInfo() + val scaleX = info.mImgRect.width() / mine.mImgRect.width() + val scaleY = info.mImgRect.height() / mine.mImgRect.height() + val scale = if (scaleX < scaleY) scaleX else scaleY + val ocx = info.mRect.left + info.mRect.width() / 2 + val ocy = info.mRect.top + info.mRect.height() / 2 + val mcx = mine.mRect.left + mine.mRect.width() / 2 + val mcy = mine.mRect.top + mine.mRect.height() / 2 + mAnimMatrix.reset() + // mAnimaMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top); + mAnimMatrix.postTranslate(ocx - mcx, ocy - mcy) + mAnimMatrix.postScale(scale, scale, ocx, ocy) + mAnimMatrix.postRotate(info.mDegrees, ocx, ocy) + executeTranslate() + mScaleCenter[ocx] = ocy + mRotateCenter[ocx] = ocy + mTranslate.withTranslate(0, 0, (-(ocx - mcx)).toInt(), (-(ocy - mcy)).toInt()) + mTranslate.withScale(scale, 1F) + mTranslate.withRotate(info.mDegrees.toInt(), 0) + if (info.mWidgetRect.width() < info.mImgRect.width() || info.mWidgetRect.height() < info.mImgRect.height()) { + var clipX = info.mWidgetRect.width() / info.mImgRect.width() + var clipY = info.mWidgetRect.height() / info.mImgRect.height() + clipX = if (clipX > 1) 1F else clipX + clipY = if (clipY > 1) 1F else clipY + val c = + if (info.mScaleType == ScaleType.FIT_START) START() else if (info.mScaleType == ScaleType.FIT_END) END() else OTHER() + mTranslate.withClip(clipX, clipY, 1 - clipX, 1 - clipY, mAnimaDuring / 3, c) + mTmpMatrix.setScale( + clipX, + clipY, + (mImgRect.left + mImgRect.right) / 2, + c.calculateTop() + ) + mTmpMatrix.mapRect(mTranslate.mClipRect, mImgRect) + mClip = mTranslate.mClipRect + } + mTranslate.start() + } else { + mFromInfo = info + mInfoTime = System.currentTimeMillis() + } + } + + fun animaTo( + info: Info, + completeCallBack: Runnable + ) { + if (isInit) { + mTranslate.stop() + mTranslateX = 0 + mTranslateY = 0 + val tcx = info.mRect.left + info.mRect.width() / 2 + val tcy = info.mRect.top + info.mRect.height() / 2 + mScaleCenter[mImgRect.left + mImgRect.width() / 2] = + mImgRect.top + mImgRect.height() / 2 + mRotateCenter.set(mScaleCenter) + + // 将图片旋转回正常位置,用以计算 + mAnimMatrix.postRotate(-mDegrees, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.mapRect(mImgRect, mBaseRect) + + // 缩放 + val scaleX = info.mImgRect.width() / mBaseRect.width() + val scaleY = info.mImgRect.height() / mBaseRect.height() + val scale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postRotate(mDegrees, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.mapRect(mImgRect, mBaseRect) + mDegrees = mDegrees % 360 + mTranslate.withTranslate( + 0, + 0, + (tcx - mScaleCenter.x).toInt(), + (tcy - mScaleCenter.y).toInt() + ) + mTranslate.withScale(mScale, scale) + mTranslate.withRotate(mDegrees.toInt(), info.mDegrees.toInt(), mAnimaDuring * 2 / 3) + if (info.mWidgetRect.width() < info.mRect.width() || info.mWidgetRect.height() < info.mRect.height()) { + var clipX = info.mWidgetRect.width() / info.mRect.width() + var clipY = info.mWidgetRect.height() / info.mRect.height() + clipX = if (clipX > 1) 1F else clipX + clipY = if (clipY > 1) 1F else clipY + val cx = clipX + val cy = clipY + val c = + if (info.mScaleType == ScaleType.FIT_START) START() else if (info.mScaleType == ScaleType.FIT_END) END() else OTHER() + postDelayed( + { mTranslate.withClip(1F, 1F, -1 + cx, -1 + cy, mAnimaDuring / 2, c) }, + mAnimaDuring / 2.toLong() + ) + } + mCompleteCallBack = completeCallBack + mTranslate.start() + } + } + + fun rotate(degrees: Float) { + mDegrees += degrees + val centerX = (mWidgetRect.left + mWidgetRect.width() / 2).toInt() + val centerY = (mWidgetRect.top + mWidgetRect.height() / 2).toInt() + mAnimMatrix.postRotate(degrees, centerX.toFloat(), centerY.toFloat()) + executeTranslate() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt b/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt new file mode 100644 index 000000000..abd6f9080 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt @@ -0,0 +1,48 @@ +package io.legado.app.ui.widget.image.photo + +import android.graphics.PointF + +import android.graphics.RectF +import android.widget.ImageView + + +class Info( + rect: RectF, + img: RectF, + widget: RectF, + base: RectF, + screenCenter: PointF, + scale: Float, + degrees: Float, + scaleType: ImageView.ScaleType? +) { + // 内部图片在整个手机界面的位置 + var mRect = RectF() + + // 控件在窗口的位置 + var mImgRect = RectF() + + var mWidgetRect = RectF() + + var mBaseRect = RectF() + + var mScreenCenter = PointF() + + var mScale = 0f + + var mDegrees = 0f + + var mScaleType: ImageView.ScaleType? = null + + init { + mRect.set(rect) + mImgRect.set(img) + mWidgetRect.set(widget) + mScale = scale + mScaleType = scaleType + mDegrees = degrees + mBaseRect.set(base) + mScreenCenter.set(screenCenter) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt new file mode 100644 index 000000000..e8d6ebf61 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt @@ -0,0 +1,56 @@ +package io.legado.app.ui.widget.image.photo + +import android.view.MotionEvent +import kotlin.math.abs +import kotlin.math.atan + +class RotateGestureDetector(l: OnRotateListener) { + + private val MAX_DEGREES_STEP = 120 + + private val mListener: OnRotateListener? = null + + private var mPrevSlope = 0f + private var mCurrSlope = 0f + + private val x1 = 0f + private val y1 = 0f + private val x2 = 0f + private val y2 = 0f + + fun onTouchEvent(event: MotionEvent) { + + when (event.actionMasked) { + MotionEvent.ACTION_POINTER_DOWN, + MotionEvent.ACTION_POINTER_UP -> { + if (event.pointerCount == 2) mPrevSlope = caculateSlope(event) + } + MotionEvent.ACTION_MOVE -> if (event.pointerCount > 1) { + mCurrSlope = caculateSlope(event) + + val currDegrees = Math.toDegrees(atan(mCurrSlope.toDouble())); + val prevDegrees = Math.toDegrees(atan(mPrevSlope.toDouble())); + + val deltaSlope = currDegrees - prevDegrees; + + if (abs(deltaSlope) <= MAX_DEGREES_STEP) { + mListener?.onRotate(deltaSlope.toFloat(), (x2 + x1) / 2, (y2 + y1) / 2); + } + mPrevSlope = mCurrSlope; + } + } + + } + + private fun caculateSlope(event: MotionEvent): Float { + val x1 = event.getX(0); + val y1 = event.getY(0); + val x2 = event.getX(1); + val y2 = event.getY(1); + return (y2 - y1) / (x2 - x1); + } +} + +interface OnRotateListener { + fun onRotate(degrees: Float, focusX: Float, focusY: Float) +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_photo_view.xml b/app/src/main/res/layout/dialog_photo_view.xml new file mode 100644 index 000000000..410ac99e5 --- /dev/null +++ b/app/src/main/res/layout/dialog_photo_view.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 20529810b..1ebe0fe81 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -719,9 +719,13 @@ 支付寶收款二維碼 QQ收款二維碼 gedoor,Invinciblelee等,详情请在github中查看 - 清除已下载书籍和字体缓存 - 默认封面 - 恢复忽略列表 - 恢复时忽略一些内容不恢复,方便不同手机配置不同 - 阅读界面设置 + 清除已下載書籍和字體緩存 + 默認封面 + 恢復忽略列表 + 恢復時忽略一些內容不恢復,方便不同手機配置不同 + 閱讀界面設置 + 图片样式(imageStyle) + 替换规则(replaceRegex) + 分組名稱 + 備註內容 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 89cdbf2a5..a600cb802 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -719,10 +719,14 @@ 支付寶收款二維碼 QQ收款二維碼 gedoor,Invinciblelee等,详情请在github中查看 - 清除已下载书籍和字体缓存 - 默认封面 - 恢复忽略列表 - 恢复时忽略一些内容不恢复,方便不同手机配置不同 - 阅读界面设置 + 清除已下載書籍和字體緩存 + 默認封面 + 恢復忽略列表 + 恢復時忽略一些內容不恢復,方便不同手機配置不同 + 閱讀界面設置 + 图片样式(imageStyle) + 替换规则(replaceRegex) + 分組名稱 + 備註內容 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index f44eaa67f..9a45c4903 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -724,4 +724,8 @@ 恢复忽略列表 恢复时忽略一些内容不恢复,方便不同手机配置不同 阅读界面设置 + 图片样式(imageStyle) + 替换规则(replaceRegex) + 分组名称 + 备注内容 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62b552f92..810277b59 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,7 +40,7 @@ in Favorites Not in Favorites Subscription - ALL + All Recent reading Last reading What\'s new @@ -55,8 +55,8 @@ Layout View Library - Import Books - Book Sources + Import books + Book sources Sources management Create/Import/Edit/Manage Book sources Settings @@ -76,17 +76,17 @@ Origin: %s Title Latest: %s - Would you like to put %s on the bookshelf? - %s text files in total + Would you like to add %s to your Bookshelf? + %s text file(s) in total Loading… Retry Web service Enable web service - web editing book sources + Edit book sources on the web http://%1$s:%2$d Offline download Offline download - Download the selected chapter to Storage + Download the selected chapter(s) to Storage Change Origin \u3000\u3000 This is an open source reading software newly developed by Kotlin, welcome to join us. Follow the WeChat Official Account [开源阅读]! @@ -101,7 +101,7 @@ Download the latest chapters automatically when updating books Backup and restore WebDav settings - WebDav settings/import legacy data + WebDav settings/Import legacy data Backup Restore Backup needs storage permission @@ -145,7 +145,7 @@ Pause Resume Timer - Speaking Pause + Speak Paused Speaking(%d min left) Hide virtual buttons when reading Hide navigation bar @@ -161,24 +161,24 @@ Background Author Author: %s - Pause + Speak Stopped Clear cache Cache cleared Save Edit source - Edit Book Sources - Disable Book Source - Add Book Source + Edit Book source + Disable Book source + Add Book source Add subscription source Add books Scan Copy source Paste source - Source rule description + Source rules description Check for Updates Scan QR code Scan local images - Rule description + Rules description Share Share to Follow system @@ -187,7 +187,7 @@ Import local sources Import online sources Replacement - Edit replacement rules + Edit replacement rule Pattern Replacement Cover @@ -204,7 +204,7 @@ Clearing the cache will delete all saved chapters. Are you sure to delete it? Book sources sharing Rule name - Pattern rule is empty or does not meet regular expression requirements + Pattern rule is empty or does not conform the regex specification. Selection action Select all Select all(%1$d/%2$d) @@ -240,7 +240,7 @@ Default font Discovery Discovery - No content.Go to Sources Management to add it! + No content.Go to Sources management to add it! Delete all Search history Clear @@ -268,8 +268,8 @@ Loading timeout Follow:%s Copied successfully - Bookshelf Arrangement - it will delete all books. Be careful,please. + Bookshelf arrangement + It will delete all books. Be careful,please. Search book sources Search subscription sources Search( %d sources in total) @@ -317,11 +317,11 @@ Auto scroll Stop Auto scroll Auto scroll speed - Book info - Edit book info + Book infomation + Edit book infomation Use Bookshelf as start page Auto jump to Recent list - Replacement object, book name or source name is available + Replacement object. Book name or source name is available Groups Cache path Clear cache @@ -330,11 +330,11 @@ Download updates Volume keys to turn page when reading Margin adjustment - Allow update + Enable update Disable update Inverse - Search bookName/author - BookName,Author,URL + Search book name/author + Book name,Author,URL FAQ Display all Discovery Display the selected origin\'s Discovery if closed @@ -410,6 +410,8 @@ 正文下一页URL规则(nextContentUrl) webJs 资源正则(sourceRegex) + 替换规则(replaceRegex) + 图片样式(imageStyle) 图标(sourceIcon) 列表规则(ruleArticles) @@ -554,7 +556,7 @@ Group selection Group editing Move to group - Add to group + Add to Groups New replacement Group Group: %s @@ -574,7 +576,7 @@ WebDav account Input your WebDav account Subscription source - Subscription source editing + Edit Subscription source Filter Search Discovery sources Current location: @@ -722,8 +724,10 @@ gedoor,Invinciblelee etc. Checking in github for details Clear the cache of the downloaded books and fonts Default cover - 恢复忽略列表 - 恢复时忽略一些内容不恢复,方便不同手机配置不同 - 阅读界面设置 + Bypass list + Ignore some contents while restoring + Reading interface settings + Group name + Remarks section \ No newline at end of file