diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 0d15693..3cc336b 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -17,6 +17,7 @@
+
@@ -130,5 +131,8 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 9d6b64e..ac6b0ae 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,5 +1,6 @@
+
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a5f05cd
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 99787d1..94a25f7 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 6b34cfb..2d8cc71 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
+apply plugin: 'AndResGuard'
android {
compileSdkVersion 30
@@ -12,13 +13,32 @@ android {
targetSdkVersion 30
versionCode 1
versionName "1.0"
+ resConfigs "zh", "zh-rCN"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+ signingConfigs {
+ release {
+ storeFile file('../../../OneDrive/swc/developer/android_key/open_key')
+ storePassword 'fumiama'
+ keyAlias 'default'
+ keyPassword 'fumiama'
+ v1SigningEnabled true
+ v2SigningEnabled true
+ }
+ }
+
buildTypes {
release {
- minifyEnabled false
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig signingConfigs.release
+ }
+ debug{
+ minifyEnabled true
+ shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@@ -27,10 +47,64 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
- implementation 'androidx.core:core-ktx:1.1.0'
- implementation 'androidx.appcompat:appcompat:1.1.0'
- testImplementation 'junit:junit:4.12'
+ implementation 'androidx.core:core-ktx:1.3.2'
+ //implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
+ implementation 'androidx.cardview:cardview:1.0.0'
+ implementation 'androidx.viewpager2:viewpager2:1.0.0'
+ //implementation 'com.google.android.material:material:1.2.1'
+ testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ implementation 'com.github.bumptech.glide:glide:4.11.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+ implementation 'com.google.code.gson:gson:2.8.6'
+}
+andResGuard {
+ // mappingFile = file("./resource_mapping.txt")
+ mappingFile = null
+ use7zip = true
+ useSign = true
+ // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
+ keepRoot = false
+ // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
+ fixedResName = "arg"
+ // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
+ mergeDuplicatedRes = true
+ whiteList = [
+ // for your icon
+ "R.drawable.icon",
+ // for fabric
+ "R.string.com.crashlytics.*",
+ // for google-services
+ "R.string.google_app_id",
+ "R.string.gcm_defaultSenderId",
+ "R.string.default_web_client_id",
+ "R.string.ga_trackingId",
+ "R.string.firebase_database_url",
+ "R.string.google_api_key",
+ "R.string.google_crash_reporting_api_key"
+ ]
+ compressFilePattern = [
+ "*.png",
+ "*.jpg",
+ "*.jpeg",
+ "*.gif",
+ ]
+ sevenzip {
+ artifact = 'com.tencent.mm:SevenZip:1.2.19'
+ //path = "/usr/local/bin/7za"
+ }
+
+ /**
+ * 可选: 如果不设置则会默认覆盖assemble输出的apk
+ **/
+ // finalApkBackupPath = "${project.rootDir}/final.apk"
+
+ /**
+ * 可选: 指定v1签名时生成jar文件的摘要算法
+ * 默认值为“SHA-1”
+ **/
+ // digestalg = "SHA-256"
}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 481bb43..b864d7d 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,44 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+-keep public class * implements com.bumptech.glide.module.GlideModule
+-keep class * extends com.bumptech.glide.module.AppGlideModule {
+ (...);
+}
+-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
+ **[] $VALUES;
+ public *;
+}
+-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
+ *** rewind();
+}
+
+##---------------Begin: proguard configuration for Gson ----------
+# Gson uses generic type information stored in a class file when working with fields. Proguard
+# removes such information by default, so configure it to keep all of it.
+-keepattributes Signature
+
+# For using GSON @Expose annotation
+-keepattributes *Annotation*
+
+# Gson specific classes
+-dontwarn sun.misc.**
+#-keep class com.google.gson.stream.** { *; }
+
+# Application classes that will be serialized/deserialized over Gson
+-keep public class top.fumiama.copymanga.data.* { *; }
+
+# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
+# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
+-keep class * extends com.google.gson.TypeAdapter
+-keep class * implements com.google.gson.TypeAdapterFactory
+-keep class * implements com.google.gson.JsonSerializer
+-keep class * implements com.google.gson.JsonDeserializer
+
+# Prevent R8 from leaving Data object members always null
+-keepclassmembers,allowobfuscation class * {
+ @com.google.gson.annotations.SerializedName ;
+}
+
+##---------------End: proguard configuration for Gson ----------
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4fa832a..93dc311 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,12 +1,22 @@
+
+
-
+ android:theme="@style/AppTheme" >
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/h.js b/app/src/main/assets/h.js
new file mode 100644
index 0000000..e748a43
--- /dev/null
+++ b/app/src/main/assets/h.js
@@ -0,0 +1,45 @@
+javascript:
+if (typeof (loaded) == "undefined"){
+ var loaded = true;
+ function scanChapters(chapter){
+ var chapterList = chapter.getElementsByClassName("table-all")[0].getElementsByTagName("a");
+ var chapterArr = Array();
+ for(var i = 0; i < chapterList.length; i++){
+ chapterArr.push(JSON.constructor());
+ chapterArr[i]["name"] = chapterList[i].title;
+ chapterArr[i]["url"] = chapterList[i].href;
+ }
+ return chapterArr;
+ }
+ function modify(){
+ var url = location.href;
+ if(url.indexOf("/chapter/")>0){
+ var imglist = document.getElementsByClassName("container-fluid comicContent")[0].getElementsByTagName("li");
+ var nextChapter = document.getElementsByClassName("comicContent-next")[0].getElementsByTagName("a")[0].href;
+ var prevChapter = document.getElementsByClassName("comicContent-prev")[1].getElementsByTagName("a")[0].href;
+ if(nextChapter == location.href) nextChapter = "null";
+ if(prevChapter == location.href) prevChapter = "null";
+ var liststr = document.title.split(" - ")[1] + " " + location.href.substring(location.href.lastIndexOf("/")+1) + "\n" + nextChapter + "\n" + prevChapter;
+ for(var i = 0; i < imglist.length; i++) liststr += "\n" + imglist[i].getElementsByTagName("img")[0].dataset.src;
+ GM.loadChapter(liststr);
+ }else {
+ var json = Array();
+ var chapters = document.getElementsByClassName("upLoop")[0].children;
+ var newObj = null;
+ for(var i = 0; i < chapters.length; i++) {
+ if(i % 2) {
+ newObj["chapters"] = scanChapters(chapters[i]);
+ json.push(newObj);
+ newObj = null;
+ }
+ else {
+ newObj = JSON.constructor();
+ newObj["name"] = chapters[i].innerText;
+ }
+ }
+ GM.setTitle(document.getElementsByTagName("h6")[0].title);
+ GM.setFab(JSON.stringify(json));
+ }
+ }
+ modify();
+}else modify();
\ No newline at end of file
diff --git a/app/src/main/assets/i.js b/app/src/main/assets/i.js
new file mode 100644
index 0000000..8d2be1a
--- /dev/null
+++ b/app/src/main/assets/i.js
@@ -0,0 +1,46 @@
+javascript:
+if (typeof (loaded) == "undefined") {
+ var loaded = true;
+ var invoke = {
+ preUrl: "",
+ pinTitle: function () {
+ /*document.getElementsByClassName("van-button__content")[2].click();*/
+ document.getElementsByClassName("indexTitle")[0].style.position = "fixed";
+ document.getElementsByClassName("indexTitle")[0].style.zIndex = 999;
+ document.getElementsByClassName("indexTitle")[0].style.width = document.body.clientWidth - 18 + "px";
+ document.getElementsByClassName("copySwiper")[0].style.marginTop = "56px";
+ document.getElementsByClassName("indexTitle")[0].style.marginTop = "-56px";
+ },
+ notCallGM: function (url) {
+ if (this.preUrl == url) return false;
+ else {
+ this.preUrl = url;
+ return true;
+ }
+ },
+ clickClass: function (name, index) { document.getElementsByClassName(name)[index].click(); },
+ clickClassCenter: function (name, index) {
+ var ev = document.createEvent('HTMLEvents');
+ ev.clientX = innerWidth / 2;
+ ev.clientY = innerHeight / 2;
+ ev.initEvent('click', false, true);
+ document.getElementsByClassName(name)[index].dispatchEvent(ev);
+ },
+ resetPreUrl: function () { this.preUrl = ""; },
+ loadChapter: function () { this.clickClassCenter("comicContentPopupImageItem", 0); GM.loadComic(location.href); },
+ urlChangeListener: function (todo) {
+ setInterval(function () { if (invoke.notCallGM(location.href)) { todo(); } }, 1000);
+ }
+ };
+ function modify() {
+ var url = location.href;
+ GM.hideFab();
+ if (url.endsWith("/index")) invoke.pinTitle();
+ else if (url.indexOf("/comicContent/") > 0) setTimeout(function () { invoke.loadChapter() }, 1000);
+ else if (url.indexOf("/details/comic/") > 0) GM.loadComic(url);
+ }
+ modify();
+ invoke.urlChangeListener(modify);
+} else {
+ setTimeout(modify, 1280);
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/activity/DlActivity.kt b/app/src/main/java/top/fumiama/copymanga/activity/DlActivity.kt
new file mode 100644
index 0000000..65d47b4
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/activity/DlActivity.kt
@@ -0,0 +1,292 @@
+package top.fumiama.copymanga.activity
+
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.Toast
+import android.widget.ToggleButton
+import kotlinx.android.synthetic.main.activity_dl.*
+import kotlinx.android.synthetic.main.button_tbutton.view.*
+import kotlinx.android.synthetic.main.line_caption.view.*
+import kotlinx.android.synthetic.main.line_horizonal.view.*
+import kotlinx.android.synthetic.main.widget_downloadbar.*
+import kotlinx.android.synthetic.main.widget_titlebar.*
+import top.fumiama.copymanga.R
+import top.fumiama.copymanga.activity.MainActivity.Companion.mh
+import top.fumiama.copymanga.handler.DlHandler
+import top.fumiama.copymanga.tool.MangaDlTools
+import top.fumiama.copymanga.tool.MangaDlTools.Companion.comicStructure
+import top.fumiama.copymanga.tool.MangaDlTools.Companion.wmdlt
+import top.fumiama.copymanga.tool.ToolsBox
+import top.fumiama.copymanga.view.LazyScrollView
+import java.io.File
+import java.lang.Thread.sleep
+import java.lang.ref.WeakReference
+
+
+class DlActivity : Activity() {
+ private var tbtncnt = 0
+ private var isNewTitle = false
+ var haveSElectAll = false
+ var checkedChapter = 0
+ var dldChapter = 0
+ var haveDlStarted = false
+ private var btnNumPerRow = 4
+ private lateinit var ltbtn: View
+ var tbtnlist: List = arrayListOf()
+ var tbtnUrlList = arrayListOf()
+ private val handler = DlHandler(this)
+ private var btnw = 0
+ private var cdwnHeight = 0
+ private var canDl = false
+ private lateinit var toolsBox: ToolsBox
+ val mangaDlTools = MangaDlTools()
+
+
+ @ExperimentalStdlibApi
+ @SuppressLint("SetTextI18n")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_dl)
+ mh?.saveUrlsOnly = true
+ handler.sendEmptyMessage(-2)
+ }
+
+ override fun onDestroy() {
+ mh?.saveUrlsOnly = false
+ wmdlt?.get()?.exit = true
+ super.onDestroy()
+ }
+
+ private fun showDlCard() {
+ //ObjectAnimator.ofFloat(csdwn, "alpha", 0.3f, 0.9f).setDuration(233).start()
+ ObjectAnimator.ofFloat(csdwn, "translationY", cdwnHeight.toFloat(), 0f).setDuration(233)
+ .start()
+ }
+
+ private fun hideDlCard() {
+ //ObjectAnimator.ofFloat(csdwn, "alpha", 0.9f, 0.3f).setDuration(233).start()
+ ObjectAnimator.ofFloat(csdwn, "translationY", 0f, cdwnHeight.toFloat()).setDuration(233)
+ .start()
+ }
+
+ private fun fillChapters() {
+ mangaDlTools.allocateChapterUrls(checkedChapter)
+ for (i in tbtnlist.indices) {
+ if (tbtnlist[i].isChecked) mangaDlTools.dlChapterUrl(tbtnUrlList[i])
+ }
+ }
+
+ private fun dlThead(dlMethod: (i: ToggleButton) -> Unit) {
+ sleep(2333)
+ for (i in tbtnlist.listIterator()) {
+ if (i.isChecked) dlMethod(i)
+ if (!canDl) break
+ }
+ if (canDl) {
+ haveDlStarted = false
+ canDl = false
+ }
+ handler.sendEmptyMessage(8)
+ }
+
+ @ExperimentalStdlibApi
+ @SuppressLint("SetTextI18n")
+ fun setLayouts() {
+ ttitle.text = comicName
+ toolsBox = ToolsBox(WeakReference(this))
+ val widthData = toolsBox.calcWidthFromDp(8, 64)
+ btnNumPerRow = widthData[0]
+ btnw = widthData[1]
+ csdwn.viewTreeObserver.addOnGlobalLayoutListener(object :
+ ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ cdwnHeight = csdwn.height
+ Log.d("MyDl", "Get csdwn height: $cdwnHeight")
+ csdwn.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ }
+ })
+ dllazys.onScrollListener = object : LazyScrollView.OnScrollListener {
+ override fun onBottom() {
+ if (csdwn.translationY > 0f) showDlCard()
+ }
+
+ override fun onScroll() {
+ if (csdwn.translationY == 0f) hideDlCard()
+ }
+
+ override fun onTop() {
+ if (csdwn.translationY > 0f) showDlCard()
+ }
+ }
+ cdwn.setOnClickListener {
+ pdwn.progress = 0
+ if (canDl || checkedChapter == 0) canDl = false
+ else {
+ haveDlStarted = true
+ canDl = true
+ handler.sendEmptyMessage(9)
+ Toast.makeText(this, "准备下载...", Toast.LENGTH_SHORT).show()
+ fillChapters()
+ Thread { dlThead { downloadChapterPages(it) } }.start()
+ }
+ }
+ cdwn.setOnLongClickListener {
+ Thread { handler.sendEmptyMessage(4) }.start()
+ return@setOnLongClickListener true
+ }
+ analyzeStructure()
+ }
+
+ private fun analyzeStructure() {
+ comicStructure?.let {
+ for (group in it) {
+ val tc = layoutInflater.inflate(R.layout.line_caption, ldwn, false)
+ tc.tcptn.text = group.name
+ ldwn.addView(
+ tc,
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ )
+ ldwn.addView(
+ layoutInflater.inflate(R.layout.div_h, ldwn, false),
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ )
+ isNewTitle = true
+ for (chapter in group.chapters) addTbtn(chapter.name, chapter.url, group.name)
+ }
+ }
+ }
+
+ @ExperimentalStdlibApi
+ private fun downloadChapterPages(i: ToggleButton) {
+ mangaDlTools.onDownloadedListener =
+ object : MangaDlTools.OnDownloadedListener {
+ override fun handleMessage(succeed: Boolean) {
+ handler.obtainMessage(if (succeed) 1 else -1, tbtnlist.indexOf(i), 0)
+ .sendToTarget()
+ }
+
+ override fun handleMessage(succeed: Boolean, pageNow: Int) {
+ handler.obtainMessage(
+ 5,
+ tbtnlist.indexOf(i),
+ pageNow,
+ succeed
+ ).sendToTarget()
+ }
+ }
+ mangaDlTools.dlChapterAndPackIntoZip(
+ File("${getExternalFilesDir("")}/$comicName/${i.hint}/${i.textOn}.zip"),
+ tbtnUrlList[tbtnlist.indexOf(i)].substringAfterLast("/")
+ )
+ }
+
+ @SuppressLint("SetTextI18n")
+ fun addTbtn(title: String, url: String, caption: String) {
+ if ((tbtncnt % btnNumPerRow == 0) || isNewTitle) {
+ ltbtn = layoutInflater.inflate(R.layout.line_horizonal, ldwn, false)
+ ldwn.addView(ltbtn)
+ tbtncnt = 0
+ isNewTitle = false
+ }
+ val tbv = layoutInflater.inflate(R.layout.button_tbutton, ltbtn.ltbtn, false)
+ tbtnlist += tbv.tbtn
+ tbtncnt++
+ tbtnUrlList.add(url)
+ tbv.tbtn.textOff = title
+ tbv.tbtn.textOn = title
+ tbv.tbtn.text = title
+ tbv.tbtn.hint = caption
+ tbv.tbtn.layoutParams.width = btnw
+ val zipf = File("${getExternalFilesDir("")}/$comicName/$caption/$title.zip")
+ if (zipf.exists()) {
+ tbv.tbtn.setBackgroundResource(R.drawable.rndbg_checked)
+ tbv.tbtn.isChecked = false
+ }
+ ltbtn.ltbtn.addView(tbv)
+ ltbtn.invalidate()
+ tbv.tbtn.setOnClickListener {
+ if (zipf.exists() && !it.tbtn.isChecked) it.tbtn.setBackgroundResource(R.drawable.rndbg_checked)
+ else it.tbtn.setBackgroundResource(R.drawable.toggle_button)
+ if (it.tbtn.isChecked) tdwn.text = "$dldChapter/${++checkedChapter}"
+ else tdwn.text = "$dldChapter/${--checkedChapter}"
+ }
+ tbv.tbtn.setOnLongClickListener {
+ if (zipf.exists()) {
+ toolsBox.buildInfo("确认删除这些章节?",
+ "该操作将不可撤销",
+ "确定",
+ null,
+ "取消",
+ {
+ Thread {
+ handler.obtainMessage(
+ 7,
+ tbtnlist.indexOf(it.tbtn),
+ 0,
+ zipf
+ ).sendToTarget()
+ }.start()
+ })
+ }
+ true
+ }
+ }
+
+ fun deleteChapters(zipf: File, index: Int) {
+ for (i in tbtnlist) {
+ if (i.isChecked) {
+ val f =
+ File("${getExternalFilesDir("")}/$comicName/${i.hint}/${i.textOn}.zip")
+ if (f.exists()) {
+ deleteChapter(f, i)
+ checkedChapter--
+ }
+ }
+ }
+ handler.sendEmptyMessage(6)
+ }
+
+ private fun deleteChapter(f: File, v: ToggleButton) {
+ f.delete()
+ v.setBackgroundResource(R.drawable.toggle_button)
+ v.isChecked = false
+ }
+
+ @SuppressLint("SetTextI18n")
+ fun updateProgressBar() {
+ tdwn.text = "${++dldChapter}/$checkedChapter"
+ setProgress2(dldChapter * 100 / checkedChapter, 233)
+ }
+
+ fun updateProgressBar(pageNow: Int, size: Int) {
+ val delta = 100 / checkedChapter
+ val start = dldChapter * delta
+ val now = pageNow * delta / size
+ setProgress2(start + now, 64)
+ }
+
+ fun setProgress2(end: Int, duration: Long) {
+ ObjectAnimator.ofInt(
+ pdwn,
+ "progress",
+ pdwn.progress,
+ end
+ ).setDuration(duration).start()
+ }
+
+ companion object {
+ var comicName = "Null"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/activity/MainActivity.kt b/app/src/main/java/top/fumiama/copymanga/activity/MainActivity.kt
new file mode 100644
index 0000000..9717d98
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/activity/MainActivity.kt
@@ -0,0 +1,53 @@
+package top.fumiama.copymanga.activity
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.os.Looper
+import android.view.View
+import android.webkit.WebView
+import kotlinx.android.synthetic.main.activity_main.*
+import top.fumiama.copymanga.R
+import top.fumiama.copymanga.handler.MainHandler
+import top.fumiama.copymanga.view.JSWebView
+import top.fumiama.copymanga.web.JS
+import top.fumiama.copymanga.web.JSHidden
+import top.fumiama.copymanga.web.WebChromeClient
+import java.lang.ref.WeakReference
+
+class MainActivity: Activity() {
+ var wh: JSWebView? = null
+ @SuppressLint("JavascriptInterface")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ wm = WeakReference(this)
+ mh = Looper.myLooper()?.let { MainHandler(it) }
+
+ WebView.setWebContentsDebuggingEnabled(true)
+ w.setWebViewClient("i.js")
+ w.webChromeClient = WebChromeClient()
+ w.loadJSInterface(JS())
+ w.loadUrl(getString(R.string.web_home))
+
+ wh = JSWebView(this, getString(R.string.pc_ua))
+ wh?.setWebViewClient("h.js")
+ wh?.loadJSInterface(JSHidden())
+ }
+
+ override fun onBackPressed() {
+ if(w.canGoBack()) w.goBack()
+ else super.onBackPressed()
+ }
+
+ fun onFabClicked(v: View){
+ startActivity(Intent(this, DlActivity::class.java))
+ }
+
+ companion object{
+ var wm: WeakReference? = null
+ var mh: MainHandler? = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/activity/ViewMangaActivity.kt b/app/src/main/java/top/fumiama/copymanga/activity/ViewMangaActivity.kt
new file mode 100644
index 0000000..23921c6
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/activity/ViewMangaActivity.kt
@@ -0,0 +1,372 @@
+package top.fumiama.copymanga.activity
+
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.os.Bundle
+import android.os.Handler
+import android.os.Message
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.SeekBar
+import android.widget.Toast
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import com.bumptech.glide.Glide
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.activity_viewmanga.*
+import kotlinx.android.synthetic.main.page_imgview.*
+import kotlinx.android.synthetic.main.page_imgview.view.*
+import kotlinx.android.synthetic.main.widget_infodrawer.*
+import kotlinx.android.synthetic.main.widget_infodrawer.view.*
+import kotlinx.android.synthetic.main.widget_titlebar.*
+import kotlinx.android.synthetic.main.widget_viewmangainfo.*
+import top.fumiama.copymanga.R
+import top.fumiama.copymanga.activity.MainActivity.Companion.wm
+import top.fumiama.copymanga.handler.TimeThread
+import top.fumiama.copymanga.tool.PropertiesTools
+import top.fumiama.copymanga.tool.ToolsBox
+import java.io.File
+import java.lang.ref.WeakReference
+import java.text.SimpleDateFormat
+import java.util.*
+
+
+class ViewMangaActivity : Activity() {
+ var count = 0
+ lateinit var handler: Handler
+ lateinit var tt: TimeThread
+ var clicked = false
+ private var isInSeek = false
+ private var useFullScreen = false
+ var r2l = true
+ private var currentItem = 0
+ private var notUseVP = true
+ private var q = 90
+ var infoDrawerDelta = 0f
+ lateinit var toolsBox: ToolsBox
+ private lateinit var p: PropertiesTools
+ var pageNum = 1
+ get() {
+ field = getPageNumber()
+ return field
+ }
+ set(value) {
+ setPageNumber(value)
+ if (notUseVP) {
+ //currentItem += delta
+ try {
+ loadOneImg()
+ } catch (e: java.lang.Exception) {
+ e.printStackTrace()
+ toolsBox.toastError("页数${currentItem}不合法")
+ }
+ }// else vp.currentItem += delta
+ field = getPageNumber()
+ }
+
+ @ExperimentalStdlibApi
+ @SuppressLint("SetTextI18n")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_viewmanga)
+ toolsBox = ToolsBox(WeakReference(this))
+ va = WeakReference(this)
+ p = PropertiesTools(File("$filesDir/settings.properties"))
+ useFullScreen = p["useFullScreen"] != "true"
+ r2l = p["r2l"] == "true"
+ //toolsBox = ToolsBox(WeakReference(this))
+ notUseVP = p["noAnimation"] == "true"
+ handler = MyHandler(infcard, toolsBox)
+ if (p["quality"] == "null") p["quality"] = "90"
+ else q = p["quality"].toInt()
+ tt = TimeThread(handler, 22)
+ tt.canDo = true
+ tt.start()
+ ttitle.text = titleText
+ try {
+ count = imgUrls.size
+ } catch (e: Exception) {
+ e.printStackTrace()
+ toolsBox.toastError("分析图片url错误")
+ }
+ try {
+ prepareItems(count)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ toolsBox.toastError("准备控件错误")
+ }
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ if (useFullScreen) window.decorView.systemUiVisibility =
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ }
+
+ private fun getPageNumber(): Int {
+ return if (r2l && !notUseVP) count - vp.currentItem
+ else (if (notUseVP) currentItem else vp.currentItem) + 1
+ }
+
+ private fun setPageNumber(num: Int) {
+ if (r2l && !notUseVP) vp.currentItem = count - num
+ else if (notUseVP) currentItem = num - 1 else vp.currentItem = num - 1
+ }
+
+ private fun loadOneImg() {
+ Glide.with(this@ViewMangaActivity.applicationContext).load(
+ imgUrls[currentItem]
+ ).thumbnail(
+ Glide.with(this@ViewMangaActivity.applicationContext).load(R.drawable.bg_comment)
+ ).into(onei)
+ updateSeekBar()
+ }
+
+ private fun setIdPosition(position: Int) {
+ infoDrawerDelta = position.toFloat()
+ infcard.translationY = infoDrawerDelta
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun prepareItems(size: Int) {
+ prepareVP()
+ prepareInfoBar(size)
+ if (notUseVP) loadOneImg() else prepareIdBtVH()
+ toolsBox.dp2px(67)?.let { setIdPosition(it) }
+ prepareIdBtFullScreen()
+ prepareIdBtVP()
+ prepareIdBtLR()
+ }
+
+ private fun prepareIdBtLR() {
+ idtblr.isChecked = r2l
+ idtblr.setOnClickListener {
+ if (idtblr.isChecked) p["r2l"] = "true"
+ else p["r2l"] = "false"
+ Toast.makeText(this.applicationContext, "下次浏览生效", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun prepareIdBtVP() {
+ idtbvp.setOnClickListener {
+ if (idtbvp.isChecked) p["noAnimation"] = "true"
+ else p["noAnimation"] = "false"
+ Toast.makeText(this.applicationContext, "下次浏览生效", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun prepareVP() {
+ if (notUseVP) {
+ vp.visibility = View.INVISIBLE
+ vone.visibility = View.VISIBLE
+ } else {
+ vp.visibility = View.VISIBLE
+ vone.visibility = View.INVISIBLE
+ vp.adapter = ViewData(vp).RecyclerViewAdapter()
+ vp.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ updateSeekBar()
+ super.onPageSelected(position)
+ }
+ })
+ if (r2l) vp.currentItem = count - 1
+ }
+ }
+
+ private fun updateSeekBar() {
+ if (!isInSeek) hideObjs()
+ updateSeekText()
+ updateSeekProgress()
+ sendProgress()
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun prepareInfoBar(size: Int) {
+ oneinfo.alpha = 0F
+ infseek.visibility = View.INVISIBLE
+ isearch.visibility = View.INVISIBLE
+ inftxtprogress.text = "$pageNum/$size"
+ infseek.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(p0: SeekBar?, p1: Int, isHuman: Boolean) {
+ if (isHuman) {
+ if (p1 >= (pageNum + 1) * 100 / size) scrollForward()
+ else if (p1 < (pageNum - 1) * 100 / size) scrollBack()
+ }
+ }
+
+ override fun onStartTrackingTouch(p0: SeekBar?) {
+ isInSeek = true
+ }
+
+ override fun onStopTrackingTouch(p0: SeekBar?) {
+ isInSeek = false
+ }
+ })
+ isearch.setOnClickListener {
+ handler.sendEmptyMessage(3)
+ }
+ }
+
+ private fun prepareIdBtVH() {
+ idtbvh.isChecked =
+ p["vertical"] == "true"
+ if (idtbvh.isChecked) vp.orientation = ViewPager2.ORIENTATION_VERTICAL
+ idtbvh.setOnClickListener {
+ if (idtbvh.isChecked) {
+ vp.orientation = ViewPager2.ORIENTATION_VERTICAL
+ p["vertical"] = "true"
+ } else {
+ vp.orientation = ViewPager2.ORIENTATION_HORIZONTAL
+ p["vertical"] = "false"
+ }
+ }
+ }
+
+ private fun prepareIdBtFullScreen() {
+ idtbfullscreen.isChecked = !useFullScreen
+ idtbfullscreen.setOnClickListener {
+ if (idtbfullscreen.isChecked) p["useFullScreen"] =
+ "true"
+ else p["useFullScreen"] = "false"
+ Toast.makeText(this.applicationContext, "下次浏览生效", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ fun scrollBack() {
+ pageNum--
+ }
+
+ fun scrollForward() {
+ pageNum++
+ }
+
+ private fun sendProgress() {
+
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun updateSeekText() {
+ inftxtprogress.text = "$pageNum/$count"
+ }
+
+ private fun updateSeekProgress() {
+ infseek.progress = pageNum * 100 / count
+ }
+
+ override fun onBackPressed() {
+ tt.canDo = false
+ wm?.get()?.w?.goBack()
+ super.onBackPressed()
+ }
+
+ override fun onDestroy() {
+ tt.canDo = false
+ super.onDestroy()
+ }
+
+ inner class ViewData(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ inner class RecyclerViewAdapter :
+ RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewData {
+ return ViewData(
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.page_imgview, parent, false)
+ )
+ }
+
+ @SuppressLint("ClickableViewAccessibility", "SetTextI18n")
+ override fun onBindViewHolder(holder: ViewData, position: Int) {
+ val pos = if (r2l) count - position - 1 else position
+ Glide.with(this@ViewMangaActivity.applicationContext).load(
+ imgUrls[pos]
+ ).thumbnail(
+ Glide.with(this@ViewMangaActivity.applicationContext)
+ .load(R.drawable.bg_comment)
+ ).into(holder.itemView.onei)
+ }
+
+ override fun getItemCount(): Int {
+ return count
+ }
+ }
+ }
+
+ fun showObjs() {
+ infseek.visibility = View.VISIBLE
+ isearch.visibility = View.VISIBLE
+ ObjectAnimator.ofFloat(
+ oneinfo,
+ "alpha",
+ oneinfo.alpha,
+ 1F
+ ).setDuration(233).start()
+ clicked = true
+ }
+
+ fun hideObjs() {
+ ObjectAnimator.ofFloat(
+ oneinfo,
+ "alpha",
+ oneinfo.alpha,
+ 0F
+ ).setDuration(233).start()
+ clicked = false
+ infseek.postDelayed({
+ infseek.visibility = View.INVISIBLE
+ isearch.visibility = View.INVISIBLE
+ }, 300)
+ handler.sendEmptyMessage(1)
+ }
+
+ class MyHandler(
+ private val infcard: View,
+ private val toolsBox: ToolsBox
+ ) : Handler() {
+ private var infcShowed = false
+ private var delta = -1f
+ get() {
+ if (field < 0) field = va?.get()?.infoDrawerDelta ?: 0f
+ return field
+ }
+
+ @SuppressLint("SimpleDateFormat", "SetTextI18n")
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ when (msg.what) {
+ 1 -> if (infcShowed) {
+ hideInfCard(); infcShowed = false
+ }
+ 2 -> if (!infcShowed) {
+ showInfCard(); infcShowed = true
+ }
+ 3 -> infcShowed = if (infcShowed) {
+ hideInfCard(); false
+ } else {
+ showInfCard(); true
+ }
+ 22 -> toolsBox.zis?.idtime?.text =
+ SimpleDateFormat("HH:mm").format(Date()) + toolsBox.week + toolsBox.netinfo
+ }
+ }
+
+ private fun showInfCard() {
+ ObjectAnimator.ofFloat(infcard.idc, "alpha", 0.3F, 0.8F).setDuration(233).start()
+ ObjectAnimator.ofFloat(infcard, "translationY", delta, 0F).setDuration(233).start()
+ }
+
+ private fun hideInfCard() {
+ ObjectAnimator.ofFloat(infcard.idc, "alpha", 0.8F, 0.3F).setDuration(233).start()
+ ObjectAnimator.ofFloat(infcard, "translationY", 0F, delta).setDuration(233).start()
+ }
+ }
+
+ companion object {
+ var va: WeakReference? = null
+ var imgUrls = arrayOf()
+ var titleText = "Null"
+ var nextChapterUrl: String? = null
+ var previousChapterUrl: String? = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/data/ComicStructure.java b/app/src/main/java/top/fumiama/copymanga/data/ComicStructure.java
new file mode 100644
index 0000000..42719f6
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/data/ComicStructure.java
@@ -0,0 +1,10 @@
+package top.fumiama.copymanga.data;
+
+public class ComicStructure {
+ public String name;
+ public Chapters[] chapters;
+ public static class Chapters{
+ public String name;
+ public String url;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/handler/DlHandler.kt b/app/src/main/java/top/fumiama/copymanga/handler/DlHandler.kt
new file mode 100644
index 0000000..b4a8d4a
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/handler/DlHandler.kt
@@ -0,0 +1,90 @@
+package top.fumiama.copymanga.handler
+
+import android.annotation.SuppressLint
+import android.os.Handler
+import android.os.Message
+import android.widget.Toast
+import kotlinx.android.synthetic.main.widget_downloadbar.*
+import top.fumiama.copymanga.R
+import top.fumiama.copymanga.activity.DlActivity
+import top.fumiama.copymanga.activity.ViewMangaActivity.Companion.imgUrls
+import top.fumiama.copymanga.tool.MangaDlTools.Companion.wmdlt
+import java.io.File
+import java.lang.ref.WeakReference
+
+class DlHandler(activity: DlActivity) : Handler() {
+ private val da = WeakReference(activity)
+ private val d = da.get()
+
+ @ExperimentalStdlibApi
+ @SuppressLint("SetTextI18n")
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ when (msg.what) {
+ -2 -> d?.setLayouts()
+ 1 -> {
+ d?.tbtnlist?.get(msg.arg1)?.setBackgroundResource(R.drawable.rndbg_checked)
+ d?.tbtnlist?.get(msg.arg1)?.isChecked = false
+ d?.updateProgressBar()
+ if (d?.haveDlStarted == false) {
+ d.dldChapter = 0
+ d.checkedChapter = 0
+ this.postDelayed({
+ d.setProgress2(0, 233)
+ d.tdwn?.text = "0/0"
+ }, 400)
+ }
+ }
+ -1 -> {
+ d?.tbtnlist?.get(msg.arg1)?.setBackgroundResource(R.drawable.rndbg_error)
+ d!!.dldChapter--
+ Toast.makeText(
+ d,
+ "下载${d.tbtnlist[msg.arg1].textOn}失败",
+ Toast.LENGTH_SHORT
+ ).show()
+ d.updateProgressBar()
+ }
+ 4 -> {
+ d?.pdwn?.progress = 0
+ if (d?.haveSElectAll == true) {
+ for (i in d.tbtnlist.listIterator()) {
+ i.setBackgroundResource(R.drawable.toggle_button)
+ i.isChecked = false
+ }
+ d.haveSElectAll = false
+ d.checkedChapter = 0
+ d.dldChapter = 0
+ } else {
+ d?.let {
+ for (i in it.tbtnlist.listIterator()) {
+ i.setBackgroundResource(R.drawable.toggle_button)
+ i.isChecked = true
+ it.checkedChapter++
+ }
+ }
+ d?.haveSElectAll = true
+ }
+ d?.tdwn?.text = "${d?.dldChapter}/${d?.checkedChapter}"
+ }
+ 5 -> {
+ d?.updateProgressBar(
+ msg.arg2,
+ wmdlt?.get()
+ ?.getImgsCountByHash(d.tbtnUrlList[msg.arg1].substringAfterLast("/")) ?: 0
+ )
+ if (!(msg.obj as Boolean)) {
+ Toast.makeText(
+ d,
+ "下载${d?.tbtnlist?.get(msg.arg1)?.textOn}的第${msg.arg2}页失败",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ 6 -> d?.tdwn?.text = "${d?.dldChapter}/${d?.checkedChapter}"
+ 7 -> d?.deleteChapters(msg.obj as File, msg.arg1)
+ 8 -> d?.cdwn?.setCardBackgroundColor(d.resources.getColor(R.color.colorBlue))
+ 9 -> d?.cdwn?.setCardBackgroundColor(d.resources.getColor(R.color.colorRed))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/handler/MainHandler.kt b/app/src/main/java/top/fumiama/copymanga/handler/MainHandler.kt
new file mode 100644
index 0000000..12fe290
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/handler/MainHandler.kt
@@ -0,0 +1,64 @@
+package top.fumiama.copymanga.handler
+
+import android.animation.ObjectAnimator
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.util.Log
+import android.view.View
+import com.google.gson.Gson
+import kotlinx.android.synthetic.main.activity_main.*
+import top.fumiama.copymanga.activity.DlActivity
+import top.fumiama.copymanga.activity.MainActivity.Companion.wm
+import top.fumiama.copymanga.activity.ViewMangaActivity
+import top.fumiama.copymanga.data.ComicStructure
+import top.fumiama.copymanga.tool.MangaDlTools
+import top.fumiama.copymanga.tool.MangaDlTools.Companion.comicStructure
+import top.fumiama.copymanga.tool.MangaDlTools.Companion.wmdlt
+
+class MainHandler(looper: Looper):Handler(looper) {
+ var saveUrlsOnly = false
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ when(msg.what){
+ 1 -> loadUrlInHiddenWebView(msg.obj as String)
+ 2 -> callViewManga(msg.obj as String)
+ 3 -> updateLoadProgress(msg.arg1)
+ 4 -> setFab(msg.obj as String)
+ 5 -> hideFab()
+ }
+ }
+ private fun loadUrlInHiddenWebView(url: String){wm?.get()?.wh?.loadUrl(url)}
+ private fun callViewManga(content: String){
+ val listChapter = content.split("\n")
+ if(!saveUrlsOnly) {
+ ViewMangaActivity.titleText = listChapter[0].substringBeforeLast(" ")
+ ViewMangaActivity.nextChapterUrl = listChapter[1].let { if(it == "null") null else it }
+ ViewMangaActivity.previousChapterUrl = listChapter[2].let { if(it == "null") null else it }
+ ViewMangaActivity.imgUrls = arrayOf()
+ for(i in 3 until listChapter.size) ViewMangaActivity.imgUrls += listChapter[i]
+ wm?.get()?.let { it.startActivity(Intent(it, ViewMangaActivity::class.java)) }
+ } else{
+ var imgs = arrayOf()
+ for(i in 3 until listChapter.size) imgs += listChapter[i]
+ wmdlt?.get()?.setChapterImgs(listChapter[0].substringAfterLast(" "), imgs)
+ }
+ }
+ private fun updateLoadProgress(progress: Int){
+ wm?.get()?.let{
+ if(it.pw.progress == 100 && progress < 100) {
+ it.pw.progress = 0
+ it.pw.visibility = View.VISIBLE
+ }
+ ObjectAnimator.ofInt(it.pw, "progress", it.pw.progress, progress).setDuration(233).start()
+ if(progress == 100) it.pw.postDelayed({it.pw.visibility = View.GONE}, 500)
+ }
+ }
+ private fun setFab(content: String){
+ //Log.d("MyMH", "Get chapter json: $content")
+ comicStructure = Gson().fromJson(content.reader(), Array::class.java)
+ wm?.get()?.fab?.visibility = View.VISIBLE
+ }
+ private fun hideFab() {wm?.get()?.fab?.visibility = View.GONE}
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/handler/TimeThread.kt b/app/src/main/java/top/fumiama/copymanga/handler/TimeThread.kt
new file mode 100644
index 0000000..270785e
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/handler/TimeThread.kt
@@ -0,0 +1,17 @@
+package top.fumiama.copymanga.handler
+
+import android.os.Handler
+
+class TimeThread(private val handler: Handler, private val msg: Int) : Thread() {
+ var canDo = false
+ override fun run() {
+ while (canDo) {
+ try {
+ handler.sendEmptyMessage(msg)
+ sleep(3000)
+ } catch (e: InterruptedException) {
+ e.printStackTrace()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/tool/DownloadTools.kt b/app/src/main/java/top/fumiama/copymanga/tool/DownloadTools.kt
new file mode 100644
index 0000000..611646c
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/tool/DownloadTools.kt
@@ -0,0 +1,37 @@
+package top.fumiama.copymanga.tool
+
+import android.util.Log
+import java.io.File
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.concurrent.Callable
+import java.util.concurrent.FutureTask
+
+class DownloadTools {
+ fun getHttpContent(Url: String, refer: String? = null): ByteArray? {
+ Log.d("Mydl", "getHttp: $Url")
+ var ret: ByteArray? = null
+ val task = FutureTask(Callable {
+ try {
+ val connection = URL(Url).openConnection() as HttpURLConnection
+ connection.requestMethod = "GET"
+ connection.connectTimeout = 10000
+ connection.readTimeout = 10000
+ refer?.let { connection.setRequestProperty("referer", it) }
+
+ ret = connection.inputStream.readBytes()
+ connection.disconnect()
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ }
+ return@Callable ret
+ })
+ Thread(task).start()
+ return try {
+ task.get()
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/tool/MangaDlTools.kt b/app/src/main/java/top/fumiama/copymanga/tool/MangaDlTools.kt
new file mode 100644
index 0000000..3dca357
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/tool/MangaDlTools.kt
@@ -0,0 +1,83 @@
+package top.fumiama.copymanga.tool
+
+import top.fumiama.copymanga.R
+import top.fumiama.copymanga.activity.MainActivity.Companion.wm
+import top.fumiama.copymanga.data.ComicStructure
+import top.fumiama.copymanga.view.JSWebView
+import top.fumiama.copymanga.web.JSHidden
+import java.io.File
+import java.lang.ref.WeakReference
+import java.util.zip.CRC32
+import java.util.zip.CheckedOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+class MangaDlTools {
+ var exit = false
+ private val p = PropertiesTools(File("${wm?.get()?.filesDir}/chapters.hash"))
+ private var imgUrlsList: Array?>? = null
+ private var chaptersCount = 0
+ private val newWebViewHidden: JSWebView?
+ get() {
+ val re = wm?.get()?.let { JSWebView(it, it.getString(R.string.pc_ua)) }
+ re?.setWebViewClient("h.js")
+ re?.loadJSInterface(JSHidden())
+ return re
+ }
+
+ init {
+ wmdlt = WeakReference(this)
+ }
+
+ fun getImgsCountByHash(hash: String): Int?{
+ return imgUrlsList?.get(p[hash].toInt())?.size
+ }
+
+ fun allocateChapterUrls(count: Int){
+ imgUrlsList = arrayOfNulls(count)
+ chaptersCount = 0
+ }
+
+ fun dlChapterUrl(url: String){
+ p[url.substringAfterLast("/")] = (chaptersCount++).toString()
+ newWebViewHidden?.loadUrl(url)
+ }
+
+ fun setChapterImgs(hash: String, imgUrls: Array){
+ imgUrlsList?.set(p[hash].toInt(), imgUrls)
+ }
+
+ fun dlChapterAndPackIntoZip(zipf: File, hash: String){
+ imgUrlsList?.get(p[hash].toInt())?.let {
+ val dl = DownloadTools()
+ zipf.parentFile?.let { if (!it.exists()) it.mkdirs() }
+ if (zipf.exists()) zipf.delete()
+ zipf.createNewFile()
+ val zip = ZipOutputStream(CheckedOutputStream(zipf.outputStream(), CRC32()))
+ zip.setLevel(9)
+ var succeed = true
+ for (i in it.indices) {
+ zip.putNextEntry(ZipEntry("$i.webp"))
+ val s = dl.getHttpContent(it[i])?.let { zip.write(it); true } ?: false
+ if (!s) succeed = s
+ onDownloadedListener?.handleMessage(s, i + 1)
+ zip.flush()
+ if (exit) break
+ }
+ zip.close()
+ onDownloadedListener?.handleMessage(succeed)
+ }
+ }
+
+ var onDownloadedListener: OnDownloadedListener? = null
+
+ interface OnDownloadedListener {
+ fun handleMessage(succeed: Boolean)
+ fun handleMessage(succeed: Boolean, pageNow: Int)
+ }
+
+ companion object {
+ var wmdlt: WeakReference? = null
+ var comicStructure: Array? = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/tool/PagesManager.kt b/app/src/main/java/top/fumiama/copymanga/tool/PagesManager.kt
new file mode 100644
index 0000000..75b682c
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/tool/PagesManager.kt
@@ -0,0 +1,60 @@
+package top.fumiama.copymanga.tool
+
+import android.widget.Toast
+import kotlinx.android.synthetic.main.activity_main.*
+import top.fumiama.copymanga.activity.MainActivity.Companion.wm
+import top.fumiama.copymanga.activity.ViewMangaActivity
+import java.lang.ref.WeakReference
+
+class PagesManager(w: WeakReference) {
+ val v = w.get()
+ private var isEndL = false
+ private var isEndR = false
+ @ExperimentalStdlibApi
+ fun toPreviousPage(){
+ toPage(v?.r2l==true)
+ }
+ @ExperimentalStdlibApi
+ fun toNextPage(){
+ toPage(v?.r2l!=true)
+ }
+ private fun judgePrevious() = v?.pageNum?:0 > 1
+ private fun judgeNext() = v?.pageNum?:0 < v?.count?:0
+ @ExperimentalStdlibApi
+ private fun toPage(goNext:Boolean){
+ val chapterUrl = if(goNext) ViewMangaActivity.nextChapterUrl else ViewMangaActivity.previousChapterUrl
+ val hint = if(goNext) "下" else "上"
+ if (v?.clicked == false) {
+ if (if(goNext)judgeNext() else judgePrevious()) {
+ if(goNext) {
+ v.scrollForward()
+ isEndR = false
+ } else {
+ v.scrollBack()
+ isEndL = false
+ }
+ } else if (chapterUrl != null) {
+ if (if(goNext)isEndR else isEndL) {
+ wm?.get()?.w?.loadUrl("javascript:invoke.clickClass(\"comicControlBottomTopClick\",${if(goNext)1 else 0});")
+ v.tt.canDo = false
+ v.finish()
+ } else {
+ Toast.makeText(
+ v.applicationContext,
+ "再次按下加载${hint}一章",
+ Toast.LENGTH_SHORT
+ ).show()
+ if(goNext) isEndR = true
+ else isEndL = true
+ }
+ } else Toast.makeText(
+ v.applicationContext,
+ "已经到头了~",
+ Toast.LENGTH_SHORT
+ ).show()
+ } else v?.hideObjs()
+ }
+ fun manageInfo(){
+ if (v?.clicked == false) v.showObjs() else v?.hideObjs()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/tool/PropertiesTools.kt b/app/src/main/java/top/fumiama/copymanga/tool/PropertiesTools.kt
new file mode 100644
index 0000000..0be5aab
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/tool/PropertiesTools.kt
@@ -0,0 +1,53 @@
+package top.fumiama.copymanga.tool
+//PropertiesTools.kt
+//created by fumiama 20200724
+import android.util.Log
+import java.io.File
+import java.io.InputStream
+import java.util.*
+
+class PropertiesTools(private val f: File):Properties() {
+ private val propfile:File
+ get() {
+ if(!f.exists()) {
+ if(f.parentFile?.exists() != true) f.parentFile?.mkdirs()
+ if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
+ createNew(f)
+ }else if(f.isDirectory) {
+ if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
+ f.delete()
+ createNew(f)
+ }
+ if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
+ if(f.parentFile?.canRead() != true) f.parentFile?.setReadable(true)
+ return f
+ }
+ private fun createNew(f: File){
+ f.createNewFile()
+ val o = f.outputStream()
+ this.storeToXML(o, "store")
+ Log.d("MyPT", "Generate new prop.")
+ o.close()
+ }
+ private fun loadFromXml(`in`: InputStream?): PropertiesTools {
+ this.loadFromXML(`in`)
+ return this
+ }
+ private fun setProp(key: String?, value: String?): PropertiesTools {
+ this.setProperty(key, value)
+ return this
+ }
+ operator fun get(key: String): String{
+ val i = propfile.inputStream()
+ val re = this.loadFromXml(i).getProperty(key)?:"null"
+ Log.d("MyPT", "Get $key = $re")
+ i.close()
+ return re
+ }
+ operator fun set(key: String, value: String){
+ val o = propfile.outputStream()
+ this.setProp(key, value).storeToXML(o, "store")
+ Log.d("MyPT", "Set $key = $value")
+ o.close()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/tool/ToolsBox.kt b/app/src/main/java/top/fumiama/copymanga/tool/ToolsBox.kt
new file mode 100644
index 0000000..d2aa189
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/tool/ToolsBox.kt
@@ -0,0 +1,91 @@
+package top.fumiama.copymanga.tool
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.widget.Toast
+import top.fumiama.copymanga.R
+import java.lang.ref.WeakReference
+import java.util.*
+import kotlin.math.sqrt
+
+class ToolsBox(w: WeakReference) {
+ val zis = (w as WeakReference).get()
+ val week: String
+ get() {
+ val cal = Calendar.getInstance()
+ return when (cal[Calendar.DAY_OF_WEEK]) {
+ 1 -> "周日"
+ 2 -> "周一"
+ 3 -> "周二"
+ 4 -> "周三"
+ 5 -> "周四"
+ 6 -> "周五"
+ 7 -> "周六"
+ else -> ""
+ }
+ }
+ val netinfo: String
+ get() {
+ val cm: ConnectivityManager =
+ zis?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ return cm.getNetworkCapabilities(cm.activeNetwork)?.let {
+ when {
+ it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> return@let "WIFI"
+ it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> return@let "移动数据"
+ it.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> return@let "蓝牙"
+ it.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> return@let "以太网"
+ it.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> return@let "LOWPAN"
+ else -> return@let "无网络"
+ }
+ } ?: "错误"
+ }
+ fun toastError(s: String, willFinish: Boolean = true) {
+ Toast.makeText(zis?.applicationContext, s, Toast.LENGTH_SHORT).show()
+ if (willFinish) zis?.finish()
+ }
+ fun buildInfo(
+ title: String,
+ msg: String,
+ txtOk: String? = null,
+ txtN: String? = null,
+ txtCancel: String? = null,
+ ok: (() -> Unit)? = null,
+ neutral: (() -> Unit)? = null,
+ cancel: (() -> Unit)? = null
+ ) {
+ val info = AlertDialog.Builder(zis)
+ info.setIcon(R.drawable.ic_launcher_foreground)
+ info.setTitle(title)
+ info.setMessage(msg)
+ txtOk?.let { info.setPositiveButton(it) { _, _ -> ok?.let { it() } } }
+ txtCancel?.let { info.setNegativeButton(it) { _, _ -> cancel?.let { it() } } }
+ txtN?.let { info.setNeutralButton(it) { _, _ -> neutral?.let { it() } } }
+ info.show()
+ }
+ fun dp2px(dp:Int):Int?{
+ return zis?.resources?.displayMetrics?.density?.let { (dp * it + 0.5).toInt()}
+ }
+ private fun px2dp(px:Int):Int?{
+ return zis?.resources?.displayMetrics?.density?.let { (px.toDouble() / it + 0.5).toInt()}
+ }
+ private fun root(a:Double, b:Double, c:Double):List?{
+ val d = b*b - 4.0 * a * c
+ if(d < 0) return null
+ val sd = sqrt(d)
+ val x1 = (-b + sd)/(2.0 * a)
+ val x2 = (-b - sd)/(2.0 * a)
+ return listOf(x1, x2)
+ }
+ fun calcWidthFromDp(marginLeftDp:Int, widthDp:Int):List{
+ val margin = marginLeftDp.toDouble()
+ val marginPx = dp2px(marginLeftDp)?:16
+ val root = root(margin, widthDp.toDouble(), -((px2dp(zis?.resources?.displayMetrics?.widthPixels?:1080))?:400).toDouble())
+ val numPerRow = root?.let { (it[0]+0.5).toInt()}?:3
+ val w = ((zis?.resources?.displayMetrics?.widthPixels?:1080)-marginPx*(numPerRow+1))/numPerRow
+ val totalWidth = ((zis?.resources?.displayMetrics?.widthPixels?:1080)-marginPx)/numPerRow
+ return listOf(numPerRow, w, totalWidth)
+ }
+}
diff --git a/app/src/main/java/top/fumiama/copymanga/view/JSWebView.kt b/app/src/main/java/top/fumiama/copymanga/view/JSWebView.kt
new file mode 100644
index 0000000..b65ebff
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/view/JSWebView.kt
@@ -0,0 +1,24 @@
+package top.fumiama.copymanga.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.util.Log
+import android.webkit.WebView
+import top.fumiama.copymanga.web.WebViewClient
+import kotlin.reflect.KClass
+
+@SuppressLint("JavascriptInterface")
+class JSWebView : WebView {
+ constructor(context: Context): super(context)
+ constructor(context: Context, attributeSet: AttributeSet): super(context, attributeSet)
+ constructor(context: Context, attributeSet: AttributeSet, defSA: Int): super(context, attributeSet, defSA)
+ constructor(context: Context, UA: String) : super(context) { settings.userAgentString = UA }
+ init {
+ settings.javaScriptEnabled = true
+ settings.domStorageEnabled = true
+ Log.d("MyJSW", "UA is: ${settings.userAgentString}")
+ }
+ fun setWebViewClient(jsFileName: String){webViewClient = WebViewClient(context, jsFileName)}
+ fun loadJSInterface(obj: Any){addJavascriptInterface(obj, "GM")}
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/view/LazyScrollView.kt b/app/src/main/java/top/fumiama/copymanga/view/LazyScrollView.kt
new file mode 100644
index 0000000..d585bad
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/view/LazyScrollView.kt
@@ -0,0 +1,48 @@
+package top.fumiama.copymanga.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.widget.ScrollView
+
+@SuppressLint("ClickableViewAccessibility")
+class LazyScrollView : ScrollView {
+ private val view: View?
+ get() = getChildAt(0)
+
+ constructor(context: Context?) : super(context)
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
+
+ init {
+ setOnTouchListener { _, event ->
+ when (event.action) {
+ MotionEvent.ACTION_UP -> this.postDelayed({
+ if (view != null && onScrollListener != null) {
+ if (onScrollListener != null) {
+ //Log.d("MyS", "view?.measuredHeight: ${view?.measuredHeight}, scrollY: $scrollY, height: $height")
+ when {
+ view?.measuredHeight?:0 <= scrollY + height -> onScrollListener?.onBottom()
+ scrollY == 0 -> onScrollListener?.onTop()
+ else -> onScrollListener?.onScroll()
+ }
+ }
+ }
+ }, 233)
+ }
+ false
+ }
+ }
+ /**
+ * 定义接口
+ * @author admin
+ */
+ interface OnScrollListener {
+ fun onBottom()
+ fun onTop()
+ fun onScroll()
+ }
+ var onScrollListener: OnScrollListener? = null
+}
diff --git a/app/src/main/java/top/fumiama/copymanga/view/ScaleImageView.kt b/app/src/main/java/top/fumiama/copymanga/view/ScaleImageView.kt
new file mode 100644
index 0000000..0ce0fc4
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/view/ScaleImageView.kt
@@ -0,0 +1,1892 @@
+package top.fumiama.copymanga.view
+
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+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.util.AttributeSet
+import android.util.Log
+import android.view.GestureDetector
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.MotionEvent
+import android.widget.ImageView
+import top.fumiama.copymanga.activity.ViewMangaActivity
+import top.fumiama.copymanga.tool.PagesManager
+import java.lang.ref.WeakReference
+import java.util.*
+import kotlin.math.sqrt
+
+/**
+ * 手势图片控件
+ *
+ * @author clifford
+ */
+class ScaleImageView : ImageView {
+ ////////////////////////////////监听器////////////////////////////////
+ /**
+ * 外界点击事件
+ *
+ * @see .setOnClickListener
+ */
+ private var mOnClickListener: OnClickListener? = null
+
+ /**
+ * 外界长按事件
+ *
+ * @see .setOnLongClickListener
+ */
+ private var mOnLongClickListener: OnLongClickListener? = null
+ override fun setOnClickListener(l: OnClickListener?) {
+ //默认的click会在任何点击情况下都会触发,所以搞成自己的
+ mOnClickListener = l
+ }
+
+ override fun setOnLongClickListener(l: OnLongClickListener?) {
+ //默认的long click会在任何长按情况下都会触发,所以搞成自己的
+ mOnLongClickListener = l
+ }
+
+ /**
+ * 外层变换矩阵,如果是单位矩阵,那么图片是fit center状态
+ *
+ * @see .getOuterMatrix
+ * @see .outerMatrixTo
+ */
+ private val mOuterMatrix = Matrix()
+
+ /**
+ * 矩形遮罩
+ *
+ * @see .getMask
+ * @see .zoomMaskTo
+ */
+ private var mMask: RectF? = null
+
+ /**
+ * 获取当前手势状态
+ *
+ * @see .PINCH_MODE_FREE
+ *
+ * @see .PINCH_MODE_SCROLL
+ *
+ * @see .PINCH_MODE_SCALE
+ */
+ /**
+ * 当前手势状态
+ *
+ * @see .getPinchMode
+ * @see .PINCH_MODE_FREE
+ *
+ * @see .PINCH_MODE_SCROLL
+ *
+ * @see .PINCH_MODE_SCALE
+ */
+ var pinchMode = PINCH_MODE_FREE
+ private set
+
+ /**
+ * 获取外部变换矩阵.
+ *
+ * 外部变换矩阵记录了图片手势操作的最终结果,是相对于图片fit center状态的变换.
+ * 默认值为单位矩阵,此时图片为fit center状态.
+ *
+ @param matrix 用于填充结果的对象
+ @return 如果传了matrix参数则将matrix填充后返回,否则new一个填充返回
+
+ fun getOuterMatrix(matrix: Matrix?): Matrix {
+ var matrix = matrix
+ if (matrix == null) {
+ matrix = Matrix(mOuterMatrix)
+ } else {
+ matrix.set(mOuterMatrix)
+ }
+ return matrix
+ }*/
+
+ /**
+ * 获取内部变换矩阵.
+ *
+ * 内部变换矩阵是原图到fit center状态的变换,当原图尺寸变化或者控件大小变化都会发生改变
+ * 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
+ *
+ * @param matrix 用于填充结果的对象
+ * @return 如果传了matrix参数则将matrix填充后返回,否则new一个填充返回
+ */
+ private fun getInnerMatrix(matrix: Matrix?): Matrix {
+ var matrix = matrix
+ if (matrix == null) {
+ matrix = Matrix()
+ } else {
+ matrix.reset()
+ }
+ if (isReady) {
+ //原图大小
+ val tempSrc = MathUtils.rectFTake(
+ 0f,
+ 0f,
+ drawable.intrinsicWidth.toFloat(),
+ drawable.intrinsicHeight.toFloat()
+ )
+ //控件大小
+ val tempDst = MathUtils.rectFTake(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat()
+ )
+ //计算fit center矩阵
+ matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER)
+ //释放临时对象
+ MathUtils.rectFGiven(tempDst)
+ MathUtils.rectFGiven(tempSrc)
+ }
+ return matrix
+ }
+
+ /**
+ * 获取图片总变换矩阵.
+ *
+ * 总变换矩阵为内部变换矩阵x外部变换矩阵,决定了原图到所见最终状态的变换
+ * 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
+ *
+ * @param matrix 用于填充结果的对象
+ * @return 如果传了matrix参数则将matrix填充后返回,否则new一个填充返回
+ *
+ * @see .getOuterMatrix
+ * @see .getInnerMatrix
+ */
+ private fun getCurrentImageMatrix(matrix: Matrix): Matrix {
+ //获取内部变换矩阵
+ var matrix = matrix
+ matrix = getInnerMatrix(matrix)
+ //乘上外部变换矩阵
+ matrix.postConcat(mOuterMatrix)
+ return matrix
+ }
+
+ /**
+ * 获取当前变换后的图片位置和尺寸
+ *
+ * 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
+ *
+ * @param rectF 用于填充结果的对象
+ * @return 如果传了rectF参数则将rectF填充后返回,否则new一个填充返回
+ *
+ * @see .getCurrentImageMatrix
+ */
+ private fun getImageBound(rectF: RectF?): RectF {
+ var rectF = rectF
+ if (rectF == null) {
+ rectF = RectF()
+ } else {
+ rectF.setEmpty()
+ }
+ return if (!isReady) {
+ rectF
+ } else {
+ //申请一个空matrix
+ val matrix = MathUtils.matrixTake()
+ //获取当前总变换矩阵
+ getCurrentImageMatrix(matrix)
+ //对原图矩形进行变换得到当前显示矩形
+ rectF[0f, 0f, drawable.intrinsicWidth.toFloat()] = drawable.intrinsicHeight.toFloat()
+ matrix.mapRect(rectF)
+ //释放临时matrix
+ MathUtils.matrixGiven(matrix)
+ rectF
+ }
+ }
+
+ /**
+ * 获取当前设置的mask
+ *
+ * @return 返回当前的mask对象副本,如果当前没有设置mask则返回null
+
+ val mask: RectF?
+ get() = if (mMask != null) {
+ RectF(mMask)
+ } else {
+ null
+ }*/
+
+ /**
+ * 与ViewPager结合的时候使用
+ * @param direction
+ * @return
+ */
+ override fun canScrollHorizontally(direction: Int): Boolean {
+ if (pinchMode == PINCH_MODE_SCALE) {
+ return true
+ }
+ val bound = getImageBound(null)
+ if (bound.isEmpty) {
+ return false
+ }
+ return if (direction > 0) {
+ bound.right > width
+ } else {
+ bound.left < 0
+ }
+ }
+
+ /**
+ * 与ViewPager结合的时候使用
+ * @param direction
+ * @return
+ */
+ override fun canScrollVertically(direction: Int): Boolean {
+ if (pinchMode == PINCH_MODE_SCALE) {
+ return true
+ }
+ val bound = getImageBound(null)
+ if (bound.isEmpty) {
+ return false
+ }
+ return if (direction > 0) {
+ bound.bottom > height
+ } else {
+ bound.top < 0
+ }
+ }
+ ////////////////////////////////公共状态设置////////////////////////////////
+ /**
+ * 执行当前outerMatrix到指定outerMatrix渐变的动画
+ *
+ * 调用此方法会停止正在进行中的手势以及手势动画.
+ * 当duration为0时,outerMatrix值会被立即设置而不会启动动画.
+ *
+ @param endMatrix 动画目标矩阵
+ @param duration 动画持续时间
+ *
+ @see .getOuterMatrix
+
+ fun outerMatrixTo(endMatrix: Matrix?, duration: Long) {
+ if (endMatrix == null) {
+ return
+ }
+ //将手势设置为PINCH_MODE_FREE将停止后续手势的触发
+ pinchMode = PINCH_MODE_FREE
+ //停止所有正在进行的动画
+ cancelAllAnimator()
+ //如果时间不合法立即执行结果
+ if (duration <= 0) {
+ mOuterMatrix.set(endMatrix)
+ dispatchOuterMatrixChanged()
+ invalidate()
+ } else {
+ //创建矩阵变化动画
+ mScaleAnimator = ScaleAnimator(mOuterMatrix, endMatrix, duration)
+ mScaleAnimator!!.start()
+ }
+ }*/
+
+ /**
+ * 执行当前mask到指定mask的变化动画
+ *
+ * 调用此方法不会停止手势以及手势相关动画,但会停止正在进行的mask动画.
+ * 当前mask为null时,则不执行动画立即设置为目标mask.
+ * 当duration为0时,立即将当前mask设置为目标mask,不会执行动画.
+ *
+ @param mask 动画目标mask
+ @param duration 动画持续时间
+ *
+ @see .getMask
+
+ fun zoomMaskTo(mask: RectF?, duration: Long) {
+ if (mask == null) {
+ return
+ }
+ //停止mask动画
+ if (mMaskAnimator != null) {
+ mMaskAnimator!!.cancel()
+ mMaskAnimator = null
+ }
+ //如果duration为0或者之前没有设置过mask,不执行动画,立即设置
+ if (duration <= 0 || mMask == null) {
+ if (mMask == null) {
+ mMask = RectF()
+ }
+ mMask!!.set(mask)
+ invalidate()
+ } else {
+ //执行mask动画
+ mMaskAnimator = MaskAnimator(mMask!!, mask, duration)
+ mMaskAnimator!!.start()
+ }
+ }*/
+
+ /**
+ * 重置所有状态
+ *
+ * 重置位置到fit center状态,清空mask,停止所有手势,停止所有动画.
+ * 但不清空drawable,以及事件绑定相关数据.
+
+ fun reset() {
+ //重置位置到fit
+ mOuterMatrix.reset()
+ dispatchOuterMatrixChanged()
+ //清空mask
+ mMask = null
+ //停止所有手势
+ pinchMode = PINCH_MODE_FREE
+ mLastMovePoint[0f] = 0f
+ mScaleCenter[0f] = 0f
+ mScaleBase = 0f
+ //停止所有动画
+ if (mMaskAnimator != null) {
+ mMaskAnimator!!.cancel()
+ mMaskAnimator = null
+ }
+ cancelAllAnimator()
+ //重绘
+ invalidate()
+ }*/
+ ////////////////////////////////对外广播事件////////////////////////////////
+ /**
+ * 外部矩阵变化事件通知监听器
+ */
+ interface OuterMatrixChangedListener {
+ /**
+ * 外部矩阵变化回调
+ *
+ * 外部矩阵的任何变化后都收到此回调.
+ * 外部矩阵变化后,总变化矩阵,图片的展示位置都将发生变化.
+ *
+ * @param pinchImageView
+ *
+ * @see .getOuterMatrix
+ * @see .getCurrentImageMatrix
+ * @see .getImageBound
+ */
+ fun onOuterMatrixChanged(pinchImageView: ScaleImageView?)
+ }
+
+ /**
+ * 所有OuterMatrixChangedListener监听列表
+ *
+ * @see .addOuterMatrixChangedListener
+ * @see .removeOuterMatrixChangedListener
+ */
+ private var mOuterMatrixChangedListeners: MutableList? =
+ null
+
+ /**
+ * 当mOuterMatrixChangedListeners被锁定不允许修改时,临时将修改写到这个副本中
+ *
+ * @see .mOuterMatrixChangedListeners
+ */
+ private var mOuterMatrixChangedListenersCopy: MutableList? =
+ null
+
+ /**
+ * mOuterMatrixChangedListeners的修改锁定
+ *
+ * 当进入dispatchOuterMatrixChanged方法时,被加1,退出前被减1
+ *
+ * @see .dispatchOuterMatrixChanged
+ * @see .addOuterMatrixChangedListener
+ * @see .removeOuterMatrixChangedListener
+ */
+ private var mDispatchOuterMatrixChangedLock = 0
+
+ /**
+ * 添加外部矩阵变化监听
+ *
+ @param listener
+
+ fun addOuterMatrixChangedListener(listener: OuterMatrixChangedListener?) {
+ if (listener == null) {
+ return
+ }
+ //如果监听列表没有被修改锁定直接将监听添加到监听列表
+ if (mDispatchOuterMatrixChangedLock == 0) {
+ if (mOuterMatrixChangedListeners == null) {
+ mOuterMatrixChangedListeners =
+ ArrayList()
+ }
+ mOuterMatrixChangedListeners!!.add(listener)
+ } else {
+ //如果监听列表修改被锁定,那么尝试在监听列表副本上添加
+ //监听列表副本将会在锁定被解除时替换到监听列表里
+ if (mOuterMatrixChangedListenersCopy == null) {
+ mOuterMatrixChangedListenersCopy = if (mOuterMatrixChangedListeners != null) {
+ ArrayList(
+ mOuterMatrixChangedListeners!!
+ )
+ } else {
+ ArrayList()
+ }
+ }
+ mOuterMatrixChangedListenersCopy!!.add(listener)
+ }
+ }*/
+
+ /**
+ * 删除外部矩阵变化监听
+ *
+ @param listener
+
+ fun removeOuterMatrixChangedListener(listener: OuterMatrixChangedListener?) {
+ if (listener == null) {
+ return
+ }
+ //如果监听列表没有被修改锁定直接在监听列表数据结构上修改
+ if (mDispatchOuterMatrixChangedLock == 0) {
+ if (mOuterMatrixChangedListeners != null) {
+ mOuterMatrixChangedListeners!!.remove(listener)
+ }
+ } else {
+ //如果监听列表被修改锁定,那么就在其副本上修改
+ //其副本将会在锁定解除时替换回监听列表
+ if (mOuterMatrixChangedListenersCopy == null) {
+ if (mOuterMatrixChangedListeners != null) {
+ mOuterMatrixChangedListenersCopy =
+ ArrayList(
+ mOuterMatrixChangedListeners!!
+ )
+ }
+ }
+ if (mOuterMatrixChangedListenersCopy != null) {
+ mOuterMatrixChangedListenersCopy!!.remove(listener)
+ }
+ }
+ }*/
+
+ /**
+ * 触发外部矩阵修改事件
+ *
+ * 需要在每次给外部矩阵设置值时都调用此方法.
+ *
+ * @see .mOuterMatrix
+ */
+ private fun dispatchOuterMatrixChanged() {
+ if (mOuterMatrixChangedListeners == null) {
+ return
+ }
+ //增加锁
+ //这里之所以用计数器做锁定是因为可能在锁定期间又间接调用了此方法产生递归
+ //使用boolean无法判断递归结束
+ mDispatchOuterMatrixChangedLock++
+ //在列表循环过程中不允许修改列表,否则将引发崩溃
+ for (listener in mOuterMatrixChangedListeners!!) {
+ listener.onOuterMatrixChanged(this)
+ }
+ //减锁
+ mDispatchOuterMatrixChangedLock--
+ //如果是递归的情况,mDispatchOuterMatrixChangedLock可能大于1,只有减到0才能算列表的锁定解除
+ if (mDispatchOuterMatrixChangedLock == 0) {
+ //如果期间有修改列表,那么副本将不为null
+ if (mOuterMatrixChangedListenersCopy != null) {
+ //将副本替换掉正式的列表
+ mOuterMatrixChangedListeners = mOuterMatrixChangedListenersCopy
+ //清空副本
+ mOuterMatrixChangedListenersCopy = null
+ }
+ }
+ }
+ ////////////////////////////////用于重载定制////////////////////////////////
+
+ /**
+ * 计算双击之后图片接下来应该被缩放的比例
+ *
+ * 如果值大于getMaxScale或者小于fit center尺寸,则实际使用取边界值.
+ * 通过覆盖此方法可以定制不同的图片被双击时使用不同的放大策略.
+ *
+ * @param innerScale 当前内部矩阵的缩放值
+ * @param outerScale 当前外部矩阵的缩放值
+ * @return 接下来的缩放比例
+ *
+ * @see .doubleTap
+ * @see .getMaxScale
+ */
+ private fun calculateNextScale(
+ innerScale: Float,
+ outerScale: Float
+ ): Float {
+ val currentScale = innerScale * outerScale
+ return if (currentScale < maxScale) {
+ maxScale
+ } else {
+ innerScale
+ }
+ }
+
+ ////////////////////////////////初始化////////////////////////////////
+ constructor(context: Context?) : super(context) {
+ initView()
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?) : super(
+ context,
+ attrs
+ ) {
+ initView()
+ }
+
+ constructor(
+ context: Context?,
+ attrs: AttributeSet?,
+ defStyle: Int
+ ) : super(context, attrs, defStyle) {
+ initView()
+ }
+
+ private fun initView() {
+ //强制设置图片scaleType为matrix
+ super.setScaleType(ScaleType.MATRIX)
+ }
+
+ //不允许设置scaleType,只能用内部设置的matrix
+ override fun setScaleType(scaleType: ScaleType) {}
+
+ ////////////////////////////////绘制////////////////////////////////
+ override fun onDraw(canvas: Canvas) {
+ try {
+ //在绘制前设置变换矩阵
+ if (isReady) {
+ val matrix = MathUtils.matrixTake()
+ imageMatrix = getCurrentImageMatrix(matrix)
+ MathUtils.matrixGiven(matrix)
+ }
+ //对图像做遮罩处理
+ if (mMask != null) {
+ canvas.save()
+ canvas.clipRect(mMask!!)
+ super.onDraw(canvas)
+ canvas.restore()
+ } else {
+ super.onDraw(canvas)
+ }
+ }catch (e:Exception){
+ e.printStackTrace()
+ ViewMangaActivity.va?.get()?.toolsBox?.toastError("图片加载错误", false)
+ }
+ }
+ ////////////////////////////////有效性判断////////////////////////////////
+ /**
+ * 判断当前情况是否能执行手势相关计算
+ *
+ * 包括:是否有图片,图片是否有尺寸,控件是否有尺寸.
+ *
+ * @return 是否能执行手势相关计算
+ */
+ private val isReady: Boolean
+ get() = drawable != null && drawable.intrinsicWidth > 0 && drawable.intrinsicHeight > 0 && width > 0 && height > 0
+ ////////////////////////////////mask动画处理////////////////////////////////
+ /**
+ * mask修改的动画
+ *
+ * 和图片的动画相互独立.
+ *
+ @see .zoomMaskTo
+ */
+ //private var mMaskAnimator: MaskAnimator? = null
+
+ /**
+ * mask变换动画
+ *
+ * 将mask从一个rect动画到另外一个rect
+
+ private inner class MaskAnimator(start: RectF, end: RectF, duration: Long) :
+ ValueAnimator(), AnimatorUpdateListener {
+ /**
+ * 开始mask
+ */
+ private val mStart = FloatArray(4)
+
+ /**
+ * 结束mask
+ */
+ private val mEnd = FloatArray(4)
+
+ /**
+ * 中间结果mask
+ */
+ private val mResult = FloatArray(4)
+ override fun onAnimationUpdate(animation: ValueAnimator) {
+ //获取动画进度,0-1范围
+ val value = animation.animatedValue as Float
+ //根据进度对起点终点之间做插值
+ for (i in 0..3) {
+ mResult[i] = mStart[i] + (mEnd[i] - mStart[i]) * value
+ }
+ //期间mask有可能被置空了,所以判断一下
+ if (mMask == null) {
+ mMask = RectF()
+ }
+ //设置新的mask并绘制
+ mMask!![mResult[0], mResult[1], mResult[2]] = mResult[3]
+ invalidate()
+ }
+
+ /**
+ * 创建mask变换动画
+ *
+ @param start 动画起始状态
+ @param end 动画终点状态
+ @param duration 动画持续时间
+ */
+ init {
+ setFloatValues(0f, 1f)
+ setDuration(duration)
+ addUpdateListener(this)
+ //将起点终点拷贝到数组方便计算
+ mStart[0] = start.left
+ mStart[1] = start.top
+ mStart[2] = start.right
+ mStart[3] = start.bottom
+ mEnd[0] = end.left
+ mEnd[1] = end.top
+ mEnd[2] = end.right
+ mEnd[3] = end.bottom
+ }
+ }*/
+ ////////////////////////////////手势动画处理////////////////////////////////
+ /**
+ * 在单指模式下:
+ * 记录上一次手指的位置,用于计算新的位置和上一次位置的差值.
+ *
+ * 双指模式下:
+ * 记录两个手指的中点,作为和mScaleCenter绑定的点.
+ * 这个绑定可以保证mScaleCenter无论如何都会跟随这个中点.
+ *
+ @see .mScaleCenter
+ *
+ @see .scale
+ @see .scaleEnd
+ */
+ private val mLastMovePoint = PointF()
+
+ /**
+ * 缩放模式下图片的缩放中点.
+ *
+ * 为其指代的点经过innerMatrix变换之后的值.
+ * 其指代的点在手势过程中始终跟随mLastMovePoint.
+ * 通过双指缩放时,其为缩放中心点.
+ *
+ * @see .saveScaleContext
+ * @see .mLastMovePoint
+ *
+ * @see .scale
+ */
+ private val mScaleCenter = PointF()
+
+ /**
+ * 缩放模式下的基础缩放比例
+ *
+ * 为外层缩放值除以开始缩放时两指距离.
+ * 其值乘上最新的两指之间距离为最新的图片缩放比例.
+ *
+ * @see .saveScaleContext
+ * @see .scale
+ */
+ private var mScaleBase = 0f
+
+ /**
+ * 图片缩放动画
+ *
+ * 缩放模式把图片的位置大小超出限制之后触发.
+ * 双击图片放大或缩小时触发.
+ * 手动调用outerMatrixTo触发.
+ *
+ * @see .scaleEnd
+ * @see .doubleTap
+ * @see .outerMatrixTo
+ */
+ private var mScaleAnimator: ScaleAnimator? = null
+
+ /**
+ * 滑动产生的惯性动画
+ *
+ * @see .fling
+ */
+ private var mFlingAnimator: FlingAnimator? = null
+
+ /**
+ * 常用手势处理
+ *
+ * 在onTouchEvent末尾被执行.
+ */
+ @ExperimentalStdlibApi
+ private val mGestureDetector =
+ GestureDetector(this.context, object : SimpleOnGestureListener() {
+ override fun onFling(
+ e1: MotionEvent,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ //只有在单指模式结束之后才允许执行fling
+ if (pinchMode == PINCH_MODE_FREE && !(mScaleAnimator != null && mScaleAnimator!!.isRunning)) {
+ //parent.requestDisallowInterceptTouchEvent(true) //触摸事件请求拦截
+ fling(velocityX, velocityY)
+ //parent.requestDisallowInterceptTouchEvent(false) //触摸事件请求取消拦截
+ }
+ return true
+ }
+
+ override fun onLongPress(e: MotionEvent) {
+ //触发长按
+ if (mOnLongClickListener != null) {
+ mOnLongClickListener!!.onLongClick(this@ScaleImageView)
+ }
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ //当手指快速第二次按下触发,此时必须是单指模式才允许执行doubleTap
+ if (pinchMode == PINCH_MODE_SCROLL && !(mScaleAnimator != null && mScaleAnimator!!.isRunning)) {
+ doubleTap(e.x, e.y)
+ }
+ return true
+ }
+
+ var v :WeakReference? = null
+ var pm: PagesManager? = null
+ override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
+ if(v == null) {
+ v = ViewMangaActivity.va
+ v?.let { pm = PagesManager(it) }
+ }
+ //触发点击
+ if (mOnClickListener != null) {
+ mOnClickListener!!.onClick(this@ScaleImageView)
+ }
+ (event.x / width).let {
+ when {
+ it <= 1.0 / 3.0 -> pm?.toPreviousPage()
+ it <= 2.0 / 3.0 -> pm?.manageInfo()
+ else -> pm?.toNextPage()
+ }
+ }
+ return true
+ }
+ })
+ private val isBig: Boolean
+ get() = MathUtils.getMatrixScale(mOuterMatrix)[0] > 1f
+
+ @ExperimentalStdlibApi
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ super.onTouchEvent(event)
+ val action = event.action and MotionEvent.ACTION_MASK
+ Log.d("MySi", "Outer Scale: ${MathUtils.getMatrixScale(mOuterMatrix)[0]}")
+ //最后一个点抬起或者取消,结束所有模式
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ //如果之前是缩放模式,还需要触发一下缩放结束动画
+ if (pinchMode == PINCH_MODE_SCALE) {
+ scaleEnd()
+ }
+ pinchMode = PINCH_MODE_FREE
+ parent.requestDisallowInterceptTouchEvent(false) //触摸事件请求取消拦截
+ } else if (action == MotionEvent.ACTION_POINTER_UP) {
+ //多个手指情况下抬起一个手指,此时需要是缩放模式才触发
+ if (pinchMode == PINCH_MODE_SCALE) {
+ //抬起的点如果大于2,那么缩放模式还有效,但是有可能初始点变了,重新测量初始点
+ if (event.pointerCount > 2) {
+ //如果还没结束缩放模式,但是第一个点抬起了,那么让第二个点和第三个点作为缩放控制点
+ if (event.action shr 8 == 0) {
+ saveScaleContext(event.getX(1), event.getY(1), event.getX(2), event.getY(2))
+ //如果还没结束缩放模式,但是第二个点抬起了,那么让第一个点和第三个点作为缩放控制点
+ } else if (event.action shr 8 == 1) {
+ saveScaleContext(event.getX(0), event.getY(0), event.getX(2), event.getY(2))
+ }
+ }
+ //如果抬起的点等于2,那么此时只剩下一个点,也不允许进入单指模式,因为此时可能图片没有在正确的位置上
+ }
+ //第一个点按下,开启滚动模式,记录开始滚动的点
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ //在矩阵动画过程中不允许启动滚动模式
+ if (!(mScaleAnimator != null && mScaleAnimator!!.isRunning)) {
+ //停止所有动画
+ cancelAllAnimator()
+ //切换到滚动模式
+ pinchMode = PINCH_MODE_SCROLL
+ //保存触发点用于move计算差值
+ mLastMovePoint[event.x] = event.y
+ }
+ //非第一个点按下,关闭滚动模式,开启缩放模式,记录缩放模式的一些初始数据
+ } else if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ //停止所有动画
+ cancelAllAnimator()
+ //切换到缩放模式
+ pinchMode = PINCH_MODE_SCALE
+ //保存缩放的两个手指
+ saveScaleContext(event.getX(0), event.getY(0), event.getX(1), event.getY(1))
+ } else if (action == MotionEvent.ACTION_MOVE) {
+ if (!(mScaleAnimator != null && mScaleAnimator!!.isRunning)) {
+ //在滚动模式下移动
+ if (pinchMode == PINCH_MODE_SCROLL) {
+ //每次移动产生一个差值累积到图片位置上
+ scrollBy(event.x - mLastMovePoint.x, event.y - mLastMovePoint.y)
+ //记录新的移动点
+ mLastMovePoint[event.x] = event.y
+ if (isBig)
+ parent.requestDisallowInterceptTouchEvent(true) //触摸事件请求拦截
+ //在缩放模式下移动
+ } else if (pinchMode == PINCH_MODE_SCALE && event.pointerCount > 1) {
+ //两个缩放点间的距离
+ val distance = MathUtils.getDistance(
+ event.getX(0),
+ event.getY(0),
+ event.getX(1),
+ event.getY(1)
+ )
+ //保存缩放点中点
+ val lineCenter = MathUtils.getCenterPoint(
+ event.getX(0),
+ event.getY(0),
+ event.getX(1),
+ event.getY(1)
+ )
+ mLastMovePoint[lineCenter[0]] = lineCenter[1]
+ //处理缩放
+ scale(mScaleCenter, mScaleBase, distance, mLastMovePoint)
+ }
+ }
+ }
+ //无论如何都处理各种外部手势
+ mGestureDetector.onTouchEvent(event)
+ return true
+ }
+
+ /**
+ * 让图片移动一段距离
+ *
+ * 不能移动超过可移动范围,超过了就到可移动范围边界为止.
+ *
+ * @param xDiff 移动距离
+ * @param yDiff 移动距离
+ * @return 是否改变了位置
+ */
+ private fun scrollBy(xDiff: Float, yDiff: Float): Boolean {
+ var xDiff = xDiff
+ var yDiff = yDiff
+ if (!isReady) {
+ return false
+ }
+ //原图方框
+ val bound = MathUtils.rectFTake()
+ getImageBound(bound)
+ //控件大小
+ val displayWidth = width.toFloat()
+ val displayHeight = height.toFloat()
+ //如果当前图片宽度小于控件宽度,则不能移动
+ when {
+ bound.right - bound.left < displayWidth -> {
+ xDiff = 0f
+ //如果图片左边在移动后超出控件左边
+ }
+ bound.left + xDiff > 0 -> {
+ //如果在移动之前是没超出的,计算应该移动的距离
+ xDiff = if (bound.left < 0) {
+ -bound.left
+ //否则无法移动
+ } else {
+ 0f
+ }
+ //如果图片右边在移动后超出控件右边
+ }
+ bound.right + xDiff < displayWidth -> {
+ //如果在移动之前是没超出的,计算应该移动的距离
+ xDiff = if (bound.right > displayWidth) {
+ displayWidth - bound.right
+ //否则无法移动
+ } else {
+ 0f
+ }
+ }
+ //以下同理
+ //应用移动变换
+ //触发重绘
+ //检查是否有变化
+ }
+ //以下同理
+ when {
+ bound.bottom - bound.top < displayHeight -> {
+ yDiff = 0f
+ }
+ bound.top + yDiff > 0 -> {
+ yDiff = if (bound.top < 0) {
+ -bound.top
+ } else {
+ 0f
+ }
+ }
+ bound.bottom + yDiff < displayHeight -> {
+ yDiff = if (bound.bottom > displayHeight) {
+ displayHeight - bound.bottom
+ } else {
+ 0f
+ }
+ }
+ //应用移动变换
+ //触发重绘
+ //检查是否有变化
+ }
+ MathUtils.rectFGiven(bound)
+ //应用移动变换
+ mOuterMatrix.postTranslate(xDiff, yDiff)
+ dispatchOuterMatrixChanged()
+ //触发重绘
+ invalidate()
+ //检查是否有变化
+ return xDiff != 0f || yDiff != 0f
+ }
+
+ /**
+ * 记录缩放前的一些信息
+ *
+ * 保存基础缩放值.
+ * 保存图片缩放中点.
+ *
+ * @param x1 缩放第一个手指
+ * @param y1 缩放第一个手指
+ * @param x2 缩放第二个手指
+ * @param y2 缩放第二个手指
+ */
+ private fun saveScaleContext(
+ x1: Float,
+ y1: Float,
+ x2: Float,
+ y2: Float
+ ) {
+ //记录基础缩放值,其中图片缩放比例按照x方向来计算
+ //理论上图片应该是等比的,x和y方向比例相同
+ //但是有可能外部设定了不规范的值.
+ //但是后续的scale操作会将xy不等的缩放值纠正,改成和x方向相同
+ mScaleBase =
+ MathUtils.getMatrixScale(mOuterMatrix)[0] / MathUtils.getDistance(x1, y1, x2, y2)
+ //两手指的中点在屏幕上落在了图片的某个点上,图片上的这个点在经过总矩阵变换后和手指中点相同
+ //现在我们需要得到图片上这个点在图片是fit center状态下在屏幕上的位置
+ //因为后续的计算都是基于图片是fit center状态下进行变换
+ //所以需要把两手指中点除以外层变换矩阵得到mScaleCenter
+ val center = MathUtils.inverseMatrixPoint(
+ MathUtils.getCenterPoint(
+ x1,
+ y1,
+ x2,
+ y2
+ ), mOuterMatrix
+ )
+ mScaleCenter[center[0]] = center[1]
+ }
+
+ /**
+ * 对图片按照一些手势信息进行缩放
+ *
+ * @param scaleCenter mScaleCenter
+ * @param scaleBase mScaleBase
+ * @param distance 手指两点之间距离
+ * @param lineCenter 手指两点之间中点
+ *
+ * @see .mScaleCenter
+ *
+ * @see .mScaleBase
+ */
+ private fun scale(
+ scaleCenter: PointF,
+ scaleBase: Float,
+ distance: Float,
+ lineCenter: PointF
+ ) {
+ if (!isReady) {
+ return
+ }
+ //计算图片从fit center状态到目标状态的缩放比例
+ val scale = scaleBase * distance
+ val matrix = MathUtils.matrixTake()
+ //按照图片缩放中心缩放,并且让缩放中心在缩放点中点上
+ matrix.postScale(scale, scale, scaleCenter.x, scaleCenter.y)
+ //让图片的缩放中点跟随手指缩放中点
+ matrix.postTranslate(lineCenter.x - scaleCenter.x, lineCenter.y - scaleCenter.y)
+ //应用变换
+ mOuterMatrix.set(matrix)
+ MathUtils.matrixGiven(matrix)
+ dispatchOuterMatrixChanged()
+ //重绘
+ invalidate()
+ }
+
+ /**
+ * 双击后放大或者缩小
+ *
+ * 将图片缩放比例缩放到nextScale指定的值.
+ * 但nextScale值不能大于最大缩放值不能小于fit center情况下的缩放值.
+ * 将双击的点尽量移动到控件中心.
+ *
+ * @param x 双击的点
+ * @param y 双击的点
+ *
+ * @see .calculateNextScale
+ * @see .getMaxScale
+ */
+ private fun doubleTap(x: Float, y: Float) {
+ if (!isReady) {
+ return
+ }
+ //获取第一层变换矩阵
+ val innerMatrix = MathUtils.matrixTake()
+ getInnerMatrix(innerMatrix)
+ //当前总的缩放比例
+ val innerScale = MathUtils.getMatrixScale(innerMatrix)[0]
+ val outerScale = MathUtils.getMatrixScale(mOuterMatrix)[0]
+ val currentScale = innerScale * outerScale
+ //控件大小
+ val displayWidth = width.toFloat()
+ val displayHeight = height.toFloat()
+ //最大放大大小
+ val maxScale = maxScale
+ //接下来要放大的大小
+ var nextScale = calculateNextScale(innerScale, outerScale)
+ //如果接下来放大大于最大值或者小于fit center值,则取边界
+ if (nextScale > maxScale) {
+ nextScale = maxScale
+ }
+ if (nextScale < innerScale) {
+ nextScale = innerScale
+ }
+ //开始计算缩放动画的结果矩阵
+ val animEnd = MathUtils.matrixTake(mOuterMatrix)
+ //计算还需缩放的倍数
+ animEnd.postScale(nextScale / currentScale, nextScale / currentScale, x, y)
+ //将放大点移动到控件中心
+ animEnd.postTranslate(displayWidth / 2f - x, displayHeight / 2f - y)
+ //得到放大之后的图片方框
+ val testMatrix = MathUtils.matrixTake(innerMatrix)
+ testMatrix.postConcat(animEnd)
+ val testBound = MathUtils.rectFTake(
+ 0f,
+ 0f,
+ drawable.intrinsicWidth.toFloat(),
+ drawable.intrinsicHeight.toFloat()
+ )
+ testMatrix.mapRect(testBound)
+ //修正位置
+ var postX = 0f
+ var postY = 0f
+ when {
+ testBound.right - testBound.left < displayWidth -> {
+ postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f
+ }
+ testBound.left > 0 -> {
+ postX = -testBound.left
+ }
+ testBound.right < displayWidth -> {
+ postX = displayWidth - testBound.right
+ }
+ //应用修正位置
+ //清理当前可能正在执行的动画
+ //启动矩阵动画
+ //清理临时变量
+ }
+ when {
+ testBound.bottom - testBound.top < displayHeight -> {
+ postY = displayHeight / 2f - (testBound.bottom + testBound.top) / 2f
+ }
+ testBound.top > 0 -> {
+ postY = -testBound.top
+ }
+ testBound.bottom < displayHeight -> {
+ postY = displayHeight - testBound.bottom
+ }
+ //应用修正位置
+ //清理当前可能正在执行的动画
+ //启动矩阵动画
+ //清理临时变量
+ }
+ //应用修正位置
+ animEnd.postTranslate(postX, postY)
+ //清理当前可能正在执行的动画
+ cancelAllAnimator()
+ //启动矩阵动画
+ mScaleAnimator = ScaleAnimator(mOuterMatrix, animEnd)
+ mScaleAnimator!!.start()
+ //清理临时变量
+ MathUtils.rectFGiven(testBound)
+ MathUtils.matrixGiven(testMatrix)
+ MathUtils.matrixGiven(animEnd)
+ MathUtils.matrixGiven(innerMatrix)
+ }
+
+ /**
+ * 当缩放操作结束动画
+ *
+ * 如果图片超过边界,找到最近的位置动画恢复.
+ * 如果图片缩放尺寸超过最大值或者最小值,找到最近的值动画恢复.
+ */
+ private fun scaleEnd() {
+ if (!isReady) {
+ return
+ }
+ //是否修正了位置
+ var change = false
+ //获取图片整体的变换矩阵
+ val currentMatrix = MathUtils.matrixTake()
+ getCurrentImageMatrix(currentMatrix)
+ //整体缩放比例
+ val currentScale =
+ MathUtils.getMatrixScale(currentMatrix)[0]
+ //第二层缩放比例
+ val outerScale = MathUtils.getMatrixScale(mOuterMatrix)[0]
+ //控件大小
+ val displayWidth = width.toFloat()
+ val displayHeight = height.toFloat()
+ //最大缩放比例
+ val maxScale = maxScale
+ //比例修正
+ var scalePost = 1f
+ //位置修正
+ var postX = 0f
+ var postY = 0f
+ //如果整体缩放比例大于最大比例,进行缩放修正
+ if (currentScale > maxScale) {
+ scalePost = maxScale / currentScale
+ }
+ //如果缩放修正后整体导致第二层缩放小于1(就是图片比fit center状态还小),重新修正缩放
+ if (outerScale * scalePost < 1f) {
+ scalePost = 1f / outerScale
+ }
+ //如果缩放修正不为1,说明进行了修正
+ if (scalePost != 1f) {
+ change = true
+ }
+ //尝试根据缩放点进行缩放修正
+ val testMatrix = MathUtils.matrixTake(currentMatrix)
+ testMatrix.postScale(scalePost, scalePost, mLastMovePoint.x, mLastMovePoint.y)
+ val testBound = MathUtils.rectFTake(
+ 0f,
+ 0f,
+ drawable.intrinsicWidth.toFloat(),
+ drawable.intrinsicHeight.toFloat()
+ )
+ //获取缩放修正后的图片方框
+ testMatrix.mapRect(testBound)
+ //检测缩放修正后位置有无超出,如果超出进行位置修正//计算结束矩阵
+ //清理当前可能正在执行的动画
+ //启动矩阵动画
+ //清理临时变量
+ //清理临时变量
+ //如果位置修正不为0,说明进行了修正
+ //只有有执行修正才执行动画
+ //如果位置修正不为0,说明进行了修正
+ //只有有执行修正才执行动画
+ when {
+ testBound.right - testBound.left < displayWidth -> {
+ postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f
+ }
+ testBound.left > 0 -> {
+ postX = -testBound.left
+ }
+ testBound.right < displayWidth -> {
+ postX = displayWidth - testBound.right
+ }
+ //计算结束矩阵
+ //清理当前可能正在执行的动画
+ //启动矩阵动画
+ //清理临时变量
+ //清理临时变量
+ }
+ //计算结束矩阵
+ //清理当前可能正在执行的动画
+ //启动矩阵动画
+ //清理临时变量
+ //清理临时变量
+ when {
+ testBound.bottom - testBound.top < displayHeight -> {
+ postY = displayHeight / 2f - (testBound.bottom + testBound.top) / 2f
+ }
+ testBound.top > 0 -> {
+ postY = -testBound.top
+ }
+ testBound.bottom < displayHeight -> {
+ postY = displayHeight - testBound.bottom
+ }
+ //如果位置修正不为0,说明进行了修正
+ //只有有执行修正才执行动画
+ }
+ //如果位置修正不为0,说明进行了修正
+ if (postX != 0f || postY != 0f) {
+ change = true
+ }
+ //只有有执行修正才执行动画
+ if (change) {
+ //计算结束矩阵
+ val animEnd = MathUtils.matrixTake(mOuterMatrix)
+ animEnd.postScale(scalePost, scalePost, mLastMovePoint.x, mLastMovePoint.y)
+ animEnd.postTranslate(postX, postY)
+ //清理当前可能正在执行的动画
+ cancelAllAnimator()
+ //启动矩阵动画
+ mScaleAnimator = ScaleAnimator(mOuterMatrix, animEnd)
+ mScaleAnimator!!.start()
+ //清理临时变量
+ MathUtils.matrixGiven(animEnd)
+ }
+ //清理临时变量
+ MathUtils.rectFGiven(testBound)
+ MathUtils.matrixGiven(testMatrix)
+ MathUtils.matrixGiven(currentMatrix)
+ }
+
+ /**
+ * 执行惯性动画
+ *
+ * 动画在遇到不能移动就停止.
+ * 动画速度衰减到很小就停止.
+ *
+ * 其中参数速度单位为 像素/秒
+ *
+ * @param vx x方向速度
+ * @param vy y方向速度
+ */
+ private fun fling(vx: Float, vy: Float) {
+ if (!isReady) {
+ return
+ }
+ //清理当前可能正在执行的动画
+ cancelAllAnimator()
+ //创建惯性动画
+ //FlingAnimator单位为 像素/帧,一秒60帧
+ mFlingAnimator = FlingAnimator(vx / 60f, vy / 60f)
+ mFlingAnimator!!.start()
+ }
+
+ /**
+ * 停止所有手势动画
+ */
+ private fun cancelAllAnimator() {
+ if (mScaleAnimator != null) {
+ mScaleAnimator!!.cancel()
+ mScaleAnimator = null
+ }
+ if (mFlingAnimator != null) {
+ mFlingAnimator!!.cancel()
+ mFlingAnimator = null
+ }
+ }
+
+ /**
+ * 惯性动画
+ *
+ * 速度逐渐衰减,每帧速度衰减为原来的FLING_DAMPING_FACTOR,当速度衰减到小于1时停止.
+ * 当图片不能移动时,动画停止.
+ */
+ private inner class FlingAnimator(vectorX: Float, vectorY: Float) :
+ ValueAnimator(), AnimatorUpdateListener {
+ /**
+ * 速度向量
+ */
+ private val mVector: FloatArray
+ override fun onAnimationUpdate(animation: ValueAnimator) {
+ //移动图像并给出结果
+ val result = scrollBy(mVector[0], mVector[1])
+ //衰减速度
+ mVector[0] *= FLING_DAMPING_FACTOR
+ mVector[1] *= FLING_DAMPING_FACTOR
+ //速度太小或者不能移动了就结束
+ if (!result || MathUtils.getDistance(
+ 0f,
+ 0f,
+ mVector[0],
+ mVector[1]
+ ) < 1f
+ ) {
+ animation.cancel()
+ }
+ }
+
+ /**
+ * 创建惯性动画
+ *
+ * 参数单位为 像素/帧
+ *
+ @param vectorX 速度向量
+ @param vectorY 速度向量
+ */
+ init {
+ setFloatValues(0f, 1f)
+ duration = 1000000
+ addUpdateListener(this)
+ mVector = floatArrayOf(vectorX, vectorY)
+ }
+ }
+
+ /**
+ * 缩放动画
+ *
+ * 在给定时间内从一个矩阵的变化逐渐动画到另一个矩阵的变化
+ */
+ private inner class ScaleAnimator @JvmOverloads constructor(
+ start: Matrix,
+ end: Matrix,
+ duration: Long = SCALE_ANIMATOR_DURATION.toLong()
+ ) :
+ ValueAnimator(), AnimatorUpdateListener {
+ /**
+ * 开始矩阵
+ */
+ private val mStart = FloatArray(9)
+
+ /**
+ * 结束矩阵
+ */
+ private val mEnd = FloatArray(9)
+
+ /**
+ * 中间结果矩阵
+ */
+ private val mResult = FloatArray(9)
+ override fun onAnimationUpdate(animation: ValueAnimator) {
+ //获取动画进度
+ val value = animation.animatedValue as Float
+ //根据动画进度计算矩阵中间插值
+ for (i in 0..8) {
+ mResult[i] = mStart[i] + (mEnd[i] - mStart[i]) * value
+ }
+ //设置矩阵并重绘
+ mOuterMatrix.setValues(mResult)
+ dispatchOuterMatrixChanged()
+ invalidate()
+ }
+ /**
+ * 构建一个缩放动画
+ *
+ * 从一个矩阵变换到另外一个矩阵
+ *
+ * @param start 开始矩阵
+ * @param end 结束矩阵
+ * @param duration 动画时间
+ */
+ /**
+ * 构建一个缩放动画
+ *
+ * 从一个矩阵变换到另外一个矩阵
+ *
+ @param start 开始矩阵
+ @param end 结束矩阵
+ */
+ init {
+ setFloatValues(0f, 1f)
+ setDuration(duration)
+ addUpdateListener(this)
+ start.getValues(mStart)
+ end.getValues(mEnd)
+ }
+ }
+ ////////////////////////////////防止内存抖动复用对象////////////////////////////////
+ /**
+ * 对象池
+ *
+ * 防止频繁new对象产生内存抖动.
+ * 由于对象池最大长度限制,如果吞度量超过对象池容量,仍然会发生抖动.
+ * 此时需要增大对象池容量,但是会占用更多内存.
+ *
+ * @param 对象池容纳的对象类型
+ */
+ private abstract class ObjectsPool(
+ /**
+ * 对象池的最大容量
+ */
+ private val mSize: Int
+ ) {
+
+ /**
+ * 对象池队列
+ */
+ private val mQueue: Queue
+
+ /**
+ * 获取一个空闲的对象
+ *
+ * 如果对象池为空,则对象池自己会new一个返回.
+ * 如果对象池内有对象,则取一个已存在的返回.
+ * take出来的对象用完要记得调用given归还.
+ * 如果不归还,让然会发生内存抖动,但不会引起泄漏.
+ *
+ * @return 可用的对象
+ *
+ * @see .given
+ */
+ fun take(): T {
+ //如果池内为空就创建一个
+ return if (mQueue.size == 0) {
+ newInstance()
+ } else {
+ //对象池里有就从顶端拿出来一个返回
+ resetInstance(mQueue.poll()?: newInstance())
+ }
+ }
+
+ /**
+ * 归还对象池内申请的对象
+ *
+ * 如果归还的对象数量超过对象池容量,那么归还的对象就会被丢弃.
+ *
+ * @param obj 归还的对象
+ *
+ * @see .take
+ */
+ fun given(obj: T?) {
+ //如果对象池还有空位子就归还对象
+ if (obj != null && mQueue.size < mSize) {
+ mQueue.offer(obj)
+ }
+ }
+
+ /**
+ * 实例化对象
+ *
+ * @return 创建的对象
+ */
+ protected abstract fun newInstance(): T
+
+ /**
+ * 重置对象
+ *
+ * 把对象数据清空到就像刚创建的一样.
+ *
+ * @param obj 需要被重置的对象
+ * @return 被重置之后的对象
+ */
+ protected abstract fun resetInstance(obj: T): T
+
+ /**
+ * 创建一个对象池
+ *
+ @param size 对象池最大容量
+ */
+ init {
+ mQueue = LinkedList()
+ }
+ }
+
+ /**
+ * 矩阵对象池
+ */
+ private class MatrixPool(size: Int) :
+ ObjectsPool(size) {
+ override fun newInstance(): Matrix {
+ return Matrix()
+ }
+
+ override fun resetInstance(obj: Matrix?): Matrix? {
+ obj?.reset()
+ return obj
+ }
+ }
+
+ /**
+ * 矩形对象池
+ */
+ private class RectFPool(size: Int) : ObjectsPool(size) {
+ override fun newInstance(): RectF {
+ return RectF()
+ }
+
+ override fun resetInstance(obj: RectF?): RectF? {
+ obj?.setEmpty()
+ return obj
+ }
+ }
+ ////////////////////////////////数学计算工具类////////////////////////////////
+ /**
+ * 数学计算工具类
+ */
+ object MathUtils {
+ /**
+ * 矩阵对象池
+ */
+ private val mMatrixPool = MatrixPool(16)
+
+ /**
+ * 获取矩阵对象
+ */
+ fun matrixTake(): Matrix {
+ return mMatrixPool.take()!!
+ }
+
+ /**
+ * 获取某个矩阵的copy
+ */
+ fun matrixTake(matrix: Matrix?): Matrix {
+ val result = mMatrixPool.take()!!
+ if (matrix != null) {
+ result.set(matrix)
+ }
+ return result
+ }
+
+ /**
+ * 归还矩阵对象
+ */
+ fun matrixGiven(matrix: Matrix) {
+ mMatrixPool.given(matrix)
+ }
+
+ /**
+ * 矩形对象池
+ */
+ private val mRectFPool = RectFPool(16)
+
+ /**
+ * 获取矩形对象
+ */
+ fun rectFTake(): RectF {
+ return mRectFPool.take()!!
+ }
+
+ /**
+ * 按照指定值获取矩形对象
+ */
+ fun rectFTake(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float
+ ): RectF {
+ val result = mRectFPool.take()!!
+ result[left, top, right] = bottom
+ return result
+ }
+
+ /**
+ * 获取某个矩形的副本
+
+ fun rectFTake(rectF: RectF?): RectF {
+ val result = mRectFPool.take()!!
+ if (rectF != null) {
+ result.set(rectF)
+ }
+ return result
+ }*/
+
+ /**
+ * 归还矩形对象
+ */
+ fun rectFGiven(rectF: RectF) {
+ mRectFPool.given(rectF)
+ }
+
+ /**
+ * 获取两点之间距离
+ *
+ * @param x1 点1
+ * @param y1 点1
+ * @param x2 点2
+ * @param y2 点2
+ * @return 距离
+ */
+ fun getDistance(
+ x1: Float,
+ y1: Float,
+ x2: Float,
+ y2: Float
+ ): Float {
+ val x = x1 - x2
+ val y = y1 - y2
+ return sqrt(x * x + y * y.toDouble()).toFloat()
+ }
+
+ /**
+ * 获取两点的中点
+ *
+ * @param x1 点1
+ * @param y1 点1
+ * @param x2 点2
+ * @param y2 点2
+ * @return float[]{x, y}
+ */
+ fun getCenterPoint(
+ x1: Float,
+ y1: Float,
+ x2: Float,
+ y2: Float
+ ): FloatArray {
+ return floatArrayOf((x1 + x2) / 2f, (y1 + y2) / 2f)
+ }
+
+ /**
+ * 获取矩阵的缩放值
+ *
+ * @param matrix 要计算的矩阵
+ * @return float[]{scaleX, scaleY}
+ */
+ fun getMatrixScale(matrix: Matrix?): FloatArray {
+ return if (matrix != null) {
+ val value = FloatArray(9)
+ matrix.getValues(value)
+ floatArrayOf(value[0], value[4])
+ } else {
+ FloatArray(2)
+ }
+ }
+
+ /**
+ * 计算点除以矩阵的值
+ *
+ * matrix.mapPoints(unknownPoint) -> point
+ * 已知point和matrix,求unknownPoint的值.
+ *
+ * @param point
+ * @param matrix
+ * @return unknownPoint
+ */
+ fun inverseMatrixPoint(
+ point: FloatArray?,
+ matrix: Matrix?
+ ): FloatArray {
+ return if (point != null && matrix != null) {
+ val dst = FloatArray(2)
+ //计算matrix的逆矩阵
+ val inverse = matrixTake()
+ matrix.invert(inverse)
+ //用逆矩阵变换point到dst,dst就是结果
+ inverse.mapPoints(dst, point)
+ //清除临时变量
+ matrixGiven(inverse)
+ dst
+ } else {
+ FloatArray(2)
+ }
+ }
+
+ /**
+ * 计算两个矩形之间的变换矩阵
+ *
+ * unknownMatrix.mapRect(to, from)
+ * 已知from矩形和to矩形,求unknownMatrix
+ *
+ @param from
+ @param to
+ @param result unknownMatrix
+
+ fun calculateRectTranslateMatrix(
+ from: RectF?,
+ to: RectF?,
+ result: Matrix?
+ ) {
+ if (from == null || to == null || result == null) {
+ return
+ }
+ if (from.width() == 0f || from.height() == 0f) {
+ return
+ }
+ result.reset()
+ result.postTranslate(-from.left, -from.top)
+ result.postScale(to.width() / from.width(), to.height() / from.height())
+ result.postTranslate(to.left, to.top)
+ }*/
+
+ /**
+ * 计算图片在某个ImageView中的显示矩形
+ *
+ @param container ImageView的Rect
+ @param srcWidth 图片的宽度
+ @param srcHeight 图片的高度
+ @param scaleType 图片在ImageView中的ScaleType
+ @param result 图片应该在ImageView中展示的矩形
+
+ fun calculateScaledRectInContainer(
+ container: RectF?,
+ srcWidth: Float,
+ srcHeight: Float,
+ scaleType: ScaleType?,
+ result: RectF?
+ ) {
+ var scaleType = scaleType
+ if (container == null || result == null) {
+ return
+ }
+ if (srcWidth == 0f || srcHeight == 0f) {
+ return
+ }
+ //默认scaleType为fit center
+ if (scaleType == null) {
+ scaleType = ScaleType.FIT_CENTER
+ }
+ result.setEmpty()
+ if (ScaleType.FIT_XY == scaleType) {
+ result.set(container)
+ } else if (ScaleType.CENTER == scaleType) {
+ val matrix = matrixTake()
+ val rect = rectFTake(0f, 0f, srcWidth, srcHeight)
+ matrix.setTranslate(
+ (container.width() - srcWidth) * 0.5f,
+ (container.height() - srcHeight) * 0.5f
+ )
+ matrix.mapRect(result, rect)
+ rectFGiven(rect)
+ matrixGiven(matrix)
+ result.left += container.left
+ result.right += container.left
+ result.top += container.top
+ result.bottom += container.top
+ } else if (ScaleType.CENTER_CROP == scaleType) {
+ val matrix = matrixTake()
+ val rect = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val scale: Float
+ var dx = 0f
+ var dy = 0f
+ if (srcWidth * container.height() > container.width() * srcHeight) {
+ scale = container.height() / srcHeight
+ dx = (container.width() - srcWidth * scale) * 0.5f
+ } else {
+ scale = container.width() / srcWidth
+ dy = (container.height() - srcHeight * scale) * 0.5f
+ }
+ matrix.setScale(scale, scale)
+ matrix.postTranslate(dx, dy)
+ matrix.mapRect(result, rect)
+ rectFGiven(rect)
+ matrixGiven(matrix)
+ result.left += container.left
+ result.right += container.left
+ result.top += container.top
+ result.bottom += container.top
+ } else if (ScaleType.CENTER_INSIDE == scaleType) {
+ val matrix = matrixTake()
+ val rect = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val scale: Float
+ val dx: Float
+ val dy: Float
+ scale = if (srcWidth <= container.width() && srcHeight <= container.height()) {
+ 1f
+ } else {
+ Math.min(
+ container.width() / srcWidth,
+ container.height() / srcHeight
+ )
+ }
+ dx = (container.width() - srcWidth * scale) * 0.5f
+ dy = (container.height() - srcHeight * scale) * 0.5f
+ matrix.setScale(scale, scale)
+ matrix.postTranslate(dx, dy)
+ matrix.mapRect(result, rect)
+ rectFGiven(rect)
+ matrixGiven(matrix)
+ result.left += container.left
+ result.right += container.left
+ result.top += container.top
+ result.bottom += container.top
+ } else if (ScaleType.FIT_CENTER == scaleType) {
+ val matrix = matrixTake()
+ val rect = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val tempSrc = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val tempDst = rectFTake(
+ 0f,
+ 0f,
+ container.width(),
+ container.height()
+ )
+ matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER)
+ matrix.mapRect(result, rect)
+ rectFGiven(tempDst)
+ rectFGiven(tempSrc)
+ rectFGiven(rect)
+ matrixGiven(matrix)
+ result.left += container.left
+ result.right += container.left
+ result.top += container.top
+ result.bottom += container.top
+ } else if (ScaleType.FIT_START == scaleType) {
+ val matrix = matrixTake()
+ val rect = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val tempSrc = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val tempDst = rectFTake(
+ 0f,
+ 0f,
+ container.width(),
+ container.height()
+ )
+ matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.START)
+ matrix.mapRect(result, rect)
+ rectFGiven(tempDst)
+ rectFGiven(tempSrc)
+ rectFGiven(rect)
+ matrixGiven(matrix)
+ result.left += container.left
+ result.right += container.left
+ result.top += container.top
+ result.bottom += container.top
+ } else if (ScaleType.FIT_END == scaleType) {
+ val matrix = matrixTake()
+ val rect = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val tempSrc = rectFTake(0f, 0f, srcWidth, srcHeight)
+ val tempDst = rectFTake(
+ 0f,
+ 0f,
+ container.width(),
+ container.height()
+ )
+ matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.END)
+ matrix.mapRect(result, rect)
+ rectFGiven(tempDst)
+ rectFGiven(tempSrc)
+ rectFGiven(rect)
+ matrixGiven(matrix)
+ result.left += container.left
+ result.right += container.left
+ result.top += container.top
+ result.bottom += container.top
+ } else {
+ result.set(container)
+ }
+ }*/
+ }
+
+ companion object {
+ ////////////////////////////////配置参数////////////////////////////////
+ /**
+ * 图片缩放动画时间
+ */
+ const val SCALE_ANIMATOR_DURATION = 200
+
+ /**
+ * 惯性动画衰减参数
+ */
+ const val FLING_DAMPING_FACTOR = 0.9f
+
+ /**
+ * 获取图片最大可放大的比例
+ *
+ * 如果放大大于这个比例则不被允许.
+ * 在双手缩放过程中如果图片放大比例大于这个值,手指释放将回弹到这个比例.
+ * 在双击放大过程中不允许放大比例大于这个值.
+ * 覆盖此方法可以定制不同情况使用不同的最大可放大比例.
+ *
+ * @return 缩放比例
+ *
+ * @see .scaleEnd
+ * @see .doubleTap
+ */
+ /**
+ * 图片最大放大比例
+ */
+ const val maxScale:Float = 2.5f
+ ////////////////////////////////公共状态获取////////////////////////////////
+ /**
+ * 手势状态:自由状态
+ *
+ * @see .getPinchMode
+ */
+ const val PINCH_MODE_FREE = 0
+
+ /**
+ * 手势状态:单指滚动状态
+ *
+ * @see .getPinchMode
+ */
+ const val PINCH_MODE_SCROLL = 1
+
+ /**
+ * 手势状态:双指缩放状态
+ *
+ * @see .getPinchMode
+ */
+ const val PINCH_MODE_SCALE = 2
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/web/JS.kt b/app/src/main/java/top/fumiama/copymanga/web/JS.kt
new file mode 100644
index 0000000..fafbb2a
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/web/JS.kt
@@ -0,0 +1,25 @@
+package top.fumiama.copymanga.web
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import top.fumiama.copymanga.R
+import top.fumiama.copymanga.activity.MainActivity.Companion.mh
+import top.fumiama.copymanga.activity.MainActivity.Companion.wm
+import top.fumiama.copymanga.activity.ViewMangaActivity
+
+class JS {
+ @JavascriptInterface
+ fun loadComic(url: String){
+ val u = when {
+ url.contains("/details/comic/") -> "${wm?.get()?.getString(R.string.web_comic_detail_pc)}${url.substringAfter("comic")}"
+ url.contains("/comicContent/") -> "${wm?.get()?.getString(R.string.web_comic_detail_pc)}/${url.substringAfter("comicContent/").substringBefore("/")}/chapter/${url.substringAfterLast("/")}"
+ else -> ""
+ }
+ Log.d("MyJS", "Load comic: $u")
+ Thread{mh?.obtainMessage(1, u)?.sendToTarget()}.start()
+ }
+ @JavascriptInterface
+ fun hideFab(){
+ Thread{mh?.sendEmptyMessage(5)}.start()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/web/JSHidden.kt b/app/src/main/java/top/fumiama/copymanga/web/JSHidden.kt
new file mode 100644
index 0000000..af7bf13
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/web/JSHidden.kt
@@ -0,0 +1,22 @@
+package top.fumiama.copymanga.web
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import top.fumiama.copymanga.activity.DlActivity
+import top.fumiama.copymanga.activity.MainActivity.Companion.mh
+
+class JSHidden {
+ @JavascriptInterface
+ fun loadChapter(listString: String){
+ Thread{mh?.obtainMessage(2, listString)?.sendToTarget()}.start()
+ }
+ @JavascriptInterface
+ fun setTitle(title:String){
+ Log.d("MyJSH", "Set title: $title")
+ DlActivity.comicName = title
+ }
+ @JavascriptInterface
+ fun setFab(content: String){
+ Thread{mh?.obtainMessage(4, content)?.sendToTarget()}.start()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/web/WebChromeClient.kt b/app/src/main/java/top/fumiama/copymanga/web/WebChromeClient.kt
new file mode 100644
index 0000000..f9b792f
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/web/WebChromeClient.kt
@@ -0,0 +1,46 @@
+package top.fumiama.copymanga.web
+
+import android.webkit.JsPromptResult
+import android.webkit.JsResult
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import top.fumiama.copymanga.activity.MainActivity.Companion.mh
+
+class WebChromeClient:WebChromeClient() {
+ override fun onProgressChanged(view: WebView?, newProgress: Int) {
+ super.onProgressChanged(view, newProgress)
+ //Log.d("MyWCC", "W progress: $newProgress")
+ Thread{mh?.obtainMessage(3, newProgress, 0)?.sendToTarget()}.start()
+ }
+
+ override fun onJsAlert(
+ view: WebView?,
+ url: String?,
+ message: String?,
+ result: JsResult?
+ ): Boolean {
+ result?.confirm()
+ return true
+ }
+
+ override fun onJsPrompt(
+ view: WebView?,
+ url: String?,
+ message: String?,
+ defaultValue: String?,
+ result: JsPromptResult?
+ ): Boolean {
+ result?.confirm()
+ return true
+ }
+
+ override fun onJsConfirm(
+ view: WebView?,
+ url: String?,
+ message: String?,
+ result: JsResult?
+ ): Boolean {
+ result?.confirm()
+ return true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/fumiama/copymanga/web/WebViewClient.kt b/app/src/main/java/top/fumiama/copymanga/web/WebViewClient.kt
new file mode 100644
index 0000000..6605af8
--- /dev/null
+++ b/app/src/main/java/top/fumiama/copymanga/web/WebViewClient.kt
@@ -0,0 +1,30 @@
+package top.fumiama.copymanga.web
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.util.Log
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.Toast
+import top.fumiama.copymanga.R
+
+class WebViewClient(private val context: Context, jsFileName: String):WebViewClient() {
+ private val js = context.assets.open(jsFileName).readBytes().decodeToString()
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ super.onPageStarted(view, url, favicon)
+ Log.d("MyWC", "Load URL: $url")
+ url?.let {
+ if(!it.startsWith(context.getString(R.string.web_home)) && !it.startsWith(context.getString(R.string.web_home_www))){
+ view?.goBack()
+ Toast.makeText(context, R.string.blocked_ad, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ url?.let {
+ view?.loadUrl(js)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/bg_comment.xml b/app/src/main/res/drawable-anydpi/bg_comment.xml
new file mode 100644
index 0000000..29a9969
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/bg_comment.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_dl.xml b/app/src/main/res/drawable-anydpi/ic_dl.xml
new file mode 100644
index 0000000..3663c9c
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_dl.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_edit.xml b/app/src/main/res/drawable-anydpi/ic_edit.xml
new file mode 100644
index 0000000..2cc2fb6
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_edit.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable-anydpi/ic_launcher_background.xml
similarity index 99%
rename from app/src/main/res/drawable/ic_launcher_background.xml
rename to app/src/main/res/drawable-anydpi/ic_launcher_background.xml
index 07d5da9..9486190 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable-anydpi/ic_launcher_background.xml
@@ -5,7 +5,7 @@
android:viewportWidth="108"
android:viewportHeight="108">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/rndbg.xml b/app/src/main/res/drawable-anydpi/rndbg.xml
new file mode 100644
index 0000000..75bc8e8
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/rndbg.xml
@@ -0,0 +1,33 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/rndbg_checked.xml b/app/src/main/res/drawable-anydpi/rndbg_checked.xml
new file mode 100644
index 0000000..9fd42dc
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/rndbg_checked.xml
@@ -0,0 +1,33 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/rndbg_error.xml b/app/src/main/res/drawable-anydpi/rndbg_error.xml
new file mode 100644
index 0000000..e3e24c0
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/rndbg_error.xml
@@ -0,0 +1,33 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/rndbg_round.xml b/app/src/main/res/drawable-anydpi/rndbg_round.xml
new file mode 100644
index 0000000..db76800
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/rndbg_round.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/rndbg_white.xml b/app/src/main/res/drawable-anydpi/rndbg_white.xml
new file mode 100644
index 0000000..4c7dcbe
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/rndbg_white.xml
@@ -0,0 +1,33 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/toggle_button.xml b/app/src/main/res/drawable-anydpi/toggle_button.xml
new file mode 100644
index 0000000..193cd6f
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/toggle_button.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-nodpi/line_colorful.webp b/app/src/main/res/drawable-nodpi/line_colorful.webp
new file mode 100644
index 0000000..63eb327
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/line_colorful.webp differ
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_dl.xml b/app/src/main/res/layout/activity_dl.xml
new file mode 100644
index 0000000..77af730
--- /dev/null
+++ b/app/src/main/res/layout/activity_dl.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..1ece29f
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_viewmanga.xml b/app/src/main/res/layout/activity_viewmanga.xml
new file mode 100644
index 0000000..f095c0e
--- /dev/null
+++ b/app/src/main/res/layout/activity_viewmanga.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/button_tbutton.xml b/app/src/main/res/layout/button_tbutton.xml
new file mode 100644
index 0000000..964645f
--- /dev/null
+++ b/app/src/main/res/layout/button_tbutton.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/app/src/main/res/layout/div_h.xml b/app/src/main/res/layout/div_h.xml
new file mode 100644
index 0000000..b1e34dd
--- /dev/null
+++ b/app/src/main/res/layout/div_h.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/line_caption.xml b/app/src/main/res/layout/line_caption.xml
new file mode 100644
index 0000000..94a6aa7
--- /dev/null
+++ b/app/src/main/res/layout/line_caption.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/line_horizonal.xml b/app/src/main/res/layout/line_horizonal.xml
new file mode 100644
index 0000000..99d2901
--- /dev/null
+++ b/app/src/main/res/layout/line_horizonal.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/page_imgview.xml b/app/src/main/res/layout/page_imgview.xml
new file mode 100644
index 0000000..eb4762a
--- /dev/null
+++ b/app/src/main/res/layout/page_imgview.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_downloadbar.xml b/app/src/main/res/layout/widget_downloadbar.xml
new file mode 100644
index 0000000..947eb22
--- /dev/null
+++ b/app/src/main/res/layout/widget_downloadbar.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_infodrawer.xml b/app/src/main/res/layout/widget_infodrawer.xml
new file mode 100644
index 0000000..68b1c55
--- /dev/null
+++ b/app/src/main/res/layout/widget_infodrawer.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_titlebar.xml b/app/src/main/res/layout/widget_titlebar.xml
new file mode 100644
index 0000000..15e34b6
--- /dev/null
+++ b/app/src/main/res/layout/widget_titlebar.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_viewmangainfo.xml b/app/src/main/res/layout/widget_viewmangainfo.xml
new file mode 100644
index 0000000..5b316bf
--- /dev/null
+++ b/app/src/main/res/layout/widget_viewmangainfo.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index eca70cf..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index a571e60..28f02b9 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index 61da551..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index c41dd28..ca36c0e 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index db5080a..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 6dba46d..db300ba 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index da31a87..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 15ac681..adff9b7 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index b216f2d..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index f25a419..cf41cee 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index e96783c..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 4faecfa..e5c1ad6 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,6 +1,12 @@
- #6200EE
- #3700B3
- #03DAC5
+ #FFCC7F
+ #8F8F8F
+ #F6837A
+ #FFCFCB
+ #C9E6A4
+ #f2faff
+ #00beff
+ #81caf8
+ #c7eaff
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bfba59b..b769034 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,10 @@
拷贝漫画
+ https://copymanga.net
+ https://www.copymanga.net
+ https://www.copymanga.net/comic
+
+ Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36 Edg/86.0.622.38
+
+ 已屏蔽其他网站页面
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index fac9291..1dcf43f 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,10 +1,9 @@
-
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index dc389d2..7026a3a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,6 +8,7 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:4.0.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.19'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/gradle.properties b/gradle.properties
index 4d15d01..98ee989 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,4 +18,5 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
-kotlin.code.style=official
\ No newline at end of file
+kotlin.code.style=official
+android.enableR8.fullMode=true
\ No newline at end of file