pull/63/head
Celeter 5 years ago
commit f6ad152e27
  1. 8
      app/build.gradle
  2. 1
      app/src/main/AndroidManifest.xml
  3. 26
      app/src/main/assets/updateLog.md
  4. 12
      app/src/main/java/io/legado/app/App.kt
  5. 27
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  6. 8
      app/src/main/java/io/legado/app/base/VMBaseActivity.kt
  7. 2
      app/src/main/java/io/legado/app/constant/Bus.kt
  8. 3
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  9. 16
      app/src/main/java/io/legado/app/constant/Theme.kt
  10. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  11. 3
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  12. 2
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  13. 2
      app/src/main/java/io/legado/app/help/ActivityHelp.kt
  14. 19
      app/src/main/java/io/legado/app/help/BookHelp.kt
  15. 18
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  16. 17
      app/src/main/java/io/legado/app/help/storage/OldRule.kt
  17. 14
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  18. 4
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  19. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt
  20. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  21. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  22. 4
      app/src/main/java/io/legado/app/model/rss/RssParser.kt
  23. 69
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  24. 2
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  25. 53
      app/src/main/java/io/legado/app/service/DownloadService.kt
  26. 3
      app/src/main/java/io/legado/app/service/WebService.kt
  27. 12
      app/src/main/java/io/legado/app/service/help/AudioPlay.kt
  28. 4
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  29. 6
      app/src/main/java/io/legado/app/ui/about/DonateActivity.kt
  30. 21
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  31. 5
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  32. 24
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  33. 3
      app/src/main/java/io/legado/app/ui/book/info/GroupSelectDialog.kt
  34. 50
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  35. 14
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  36. 11
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  37. 18
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  38. 5
      app/src/main/java/io/legado/app/ui/book/read/page/ContentSelectActionCallback.kt
  39. 3
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  40. 8
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  41. 4
      app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt
  42. 4
      app/src/main/java/io/legado/app/ui/book/read/page/PageFactory.kt
  43. 27
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  44. 4
      app/src/main/java/io/legado/app/ui/book/read/page/TextChapter.kt
  45. 2
      app/src/main/java/io/legado/app/ui/book/read/page/TextPage.kt
  46. 7
      app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt
  47. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt
  48. 2
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/EventExtensions.kt
  49. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  50. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt
  51. 10
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  52. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  53. 526
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  54. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt
  55. 28
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  56. 2
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  57. 3
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  58. 3
      app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt
  59. 1
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt
  60. 2
      app/src/main/java/io/legado/app/ui/chapterlist/ChapterListActivity.kt
  61. 8
      app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt
  62. 2
      app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt
  63. 45
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  64. 71
      app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt
  65. 36
      app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt
  66. 3
      app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt
  67. 11
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  68. 3
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  69. 8
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  70. 3
      app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt
  71. 2
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  72. 17
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  73. 3
      app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt
  74. 3
      app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt
  75. 1
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt
  76. 58
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  77. 13
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  78. 3
      app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt
  79. 2
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  80. 50
      app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
  81. 954
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java
  82. 191
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.kt
  83. 221
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.kt
  84. 769
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.kt
  85. 126
      app/src/main/java/io/legado/app/ui/widget/page/delegate/SimulationPageDelegate.kt
  86. 26
      app/src/main/java/io/legado/app/utils/MenuExtensions.kt
  87. 4
      app/src/main/java/io/legado/app/web/controller/SourceController.kt
  88. BIN
      app/src/main/res/drawable/bg_shadow_bottom.png
  89. 8
      app/src/main/res/drawable/bg_shadow_bottom.xml
  90. BIN
      app/src/main/res/drawable/bg_shadow_bottom_night.png
  91. 8
      app/src/main/res/drawable/bg_shadow_bottom_night.xml
  92. BIN
      app/src/main/res/drawable/bg_shadow_top.png
  93. 8
      app/src/main/res/drawable/bg_shadow_top.xml
  94. BIN
      app/src/main/res/drawable/bg_shadow_top_night.png
  95. 8
      app/src/main/res/drawable/bg_shadow_top_night.xml
  96. 3
      app/src/main/res/drawable/item_bg_dark.xml
  97. 3
      app/src/main/res/drawable/item_bg_light.xml
  98. 9
      app/src/main/res/drawable/side_nav_bar.xml
  99. 14
      app/src/main/res/layout/activity_audio_play.xml
  100. 30
      app/src/main/res/layout/activity_book_info.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -37,6 +37,7 @@ android {
versionName version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true
}
buildTypes {
release {
@ -98,7 +99,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.2.0-alpha02'
implementation 'com.google.android.material:material:1.2.0-alpha03'
implementation 'com.google.android:flexbox:1.1.0'
//lifecycle
@ -107,12 +108,12 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room
def room_version = '2.2.2'
def room_version = '2.2.3'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//paging
implementation 'androidx.paging:paging-runtime:2.1.0'
implementation 'androidx.paging:paging-runtime:2.1.1'
//anko
def anko_version = '0.10.8'
@ -159,6 +160,7 @@ dependencies {
//MarkDown
implementation 'ru.noties.markwon:core:3.0.2'
}
apply plugin: 'com.google.gms.google-services'

@ -102,6 +102,7 @@
<activity android:name=".ui.rss.source.manage.RssSourceActivity" />
<activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity" />
<activity android:name=".ui.download.DownloadActivity" />
<activity
android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label">

@ -1,12 +1,36 @@
## 更新日志
* 旧版数据导入教程:
* 先在旧版阅读2.19进行备份,再在新版阅读3.0的【我的】中,点击【备份与恢复】,选择【导入旧版本数据】,提示需要存储权限,选择允许即可导入成功。
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】,提示存储权限,选择允许即可导入成功。
* 注意:由于安卓10更改了权限策略,还需要给「允许安装其他应用」的权限才能导入源。MIUI11也需要此权限。
**2019/12/28**
* 添加下载界面
* 添加分组备份
**2019/12/23**
* 修复每次打开翻页模式恢复默认的bug
* 修复m3u8报错问题
**2019/12/22**
* 更新音频播放界面
**2019/12/20**
* 阅读界面的下载菜单可以用了
* rss阅读bug修复,webView内可以用返回建返回上一链接
* 优化搜索界面逻辑
* 导入旧书源时url里的&不再转为&&
* 修复书架封面显示不全的bug
**2019/12/16**
* 添加几个主题选择
* 音频播放添加header支持
**2019/12/15**
* 修复清理缓存会把其他文件删除的问题
* 详情页模糊背景
* 修复朗读时最后一页显示不正确的问题
* 最近感冒了,发热咳嗽还没好,继续咸鱼
**2019/12/12**
* [fix]web服务停止问题

@ -18,7 +18,7 @@ import io.legado.app.help.ActivityHelp
import io.legado.app.help.CrashHandler
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.widget.page.ChapterProvider
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.isNightTheme
@ -70,22 +70,22 @@ class App : Application() {
if (isNightTheme) {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_grey_900))
getPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color))
)
.accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
getPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color))
)
.backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.md_black_1000))
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
)
.apply()
} else {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500))
getPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100))
)
.accentColor(
getPrefInt("colorAccent", getCompatColor(R.color.md_pink_800))
getPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color))
)
.backgroundColor(
getPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))

@ -7,6 +7,7 @@ import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import io.legado.app.R
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ColorUtils
import io.legado.app.lib.theme.primaryColor
@ -19,8 +20,11 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
abstract class BaseActivity(private val layoutID: Int, private val fullScreen: Boolean = true) :
AppCompatActivity(),
abstract class BaseActivity(
private val layoutID: Int,
private val fullScreen: Boolean = true,
private val theme: Theme = Theme.Auto
) : AppCompatActivity(),
CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -43,7 +47,7 @@ abstract class BaseActivity(private val layoutID: Int, private val fullScreen: B
final override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return menu?.let {
val bool = onCompatCreateOptionsMenu(it)
it.applyTint(this)
it.applyTint(this, theme)
bool
} ?: super.onCreateOptionsMenu(menu)
}
@ -73,10 +77,14 @@ abstract class BaseActivity(private val layoutID: Int, private val fullScreen: B
private fun initTheme() {
ATH.applyBackgroundTint(window.decorView)
if (ColorUtils.isColorLight(primaryColor)) {
setTheme(R.style.AppTheme_Light)
} else {
setTheme(R.style.AppTheme_Dark)
when (theme) {
Theme.Dark -> setTheme(R.style.AppTheme_Dark)
Theme.Light -> setTheme(R.style.AppTheme_Light)
else -> if (ColorUtils.isColorLight(primaryColor)) {
setTheme(R.style.AppTheme_Light)
} else {
setTheme(R.style.AppTheme_Dark)
}
}
}
@ -91,6 +99,11 @@ abstract class BaseActivity(private val layoutID: Int, private val fullScreen: B
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
ATH.setStatusBarColorAuto(this, fullScreen)
if (theme == Theme.Dark) {
ATH.setLightStatusBar(this, false)
} else if (theme == Theme.Light) {
ATH.setLightStatusBar(this, true)
}
}
open fun observeLiveBus() {

@ -1,9 +1,13 @@
package io.legado.app.base
import androidx.lifecycle.ViewModel
import io.legado.app.constant.Theme
abstract class VMBaseActivity<VM : ViewModel>(layoutID: Int, fullScreen: Boolean = true) :
BaseActivity(layoutID, fullScreen) {
abstract class VMBaseActivity<VM : ViewModel>(
layoutID: Int,
fullScreen: Boolean = true,
theme: Theme = Theme.Auto
) : BaseActivity(layoutID, fullScreen, theme) {
protected abstract val viewModel: VM

@ -18,4 +18,6 @@ object Bus {
const val AUDIO_SIZE = "audioSize"
const val AUDIO_SPEED = "audioSpeed"
const val SHOW_RSS = "showRss"
const val WEB_SERVICE_STOP = "webServiceStop"
const val UP_DOWNLOAD = "upDownload"
}

@ -13,4 +13,7 @@ object PreferKey {
const val recordLog = "recordLog"
const val processText = "process_text"
const val cleanCache = "cleanCache"
const val lastGroup = "lastGroup"
const val pageAnim = "pageAnim"
const val readBookFont = "readBookFont"
}

@ -0,0 +1,16 @@
package io.legado.app.constant
import io.legado.app.App
import io.legado.app.utils.isNightTheme
enum class Theme {
Dark, Light, Auto;
companion object {
fun getTheme(): Theme {
return if (App.INSTANCE.isNightTheme) {
Dark
} else Light
}
}
}

@ -17,6 +17,9 @@ interface BookDao {
@Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc")
fun observeLocal(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0 order by durChapterTime desc")
fun observeDownload(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE `group` = :group")
fun observeByGroup(group: Int): LiveData<List<Book>>

@ -16,6 +16,9 @@ interface BookGroupDao {
@get:Query("SELECT MAX(groupId) FROM book_groups")
val maxId: Int
@Query("SELECT * FROM book_groups ORDER BY `order`")
fun all(): List<BookGroup>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookGroup: BookGroup)

@ -75,7 +75,7 @@ interface BookSourceDao {
fun update(vararg bookSource: BookSource)
@Delete
fun delete(vararg bookSource: BookSource)
fun delete(bookSource: BookSource)
@Query("delete from book_sources where bookSourceUrl = :key")
fun delete(key: String)

@ -1,7 +1,6 @@
package io.legado.app.help
import android.app.Activity
import io.legado.app.utils.LogUtils
import java.lang.ref.WeakReference
import java.util.*
@ -13,7 +12,6 @@ object ActivityHelp {
private val activities: MutableList<WeakReference<Activity>> = arrayListOf()
fun size(): Int {
LogUtils.d("xxxx", activities.size.toString())
return activities.size
}

@ -35,13 +35,24 @@ object BookHelp {
FileHelp.getFolder(getBookCachePath())
}
@Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return
FileHelp.getFolder(getBookFolder(book)).listFiles()?.forEach {
if (it.name.startsWith(String.format("%05d", bookChapter.index))) {
it.delete()
return@forEach
}
}
val filePath = getChapterPath(book, bookChapter)
val file = FileHelp.getFile(filePath)
file.writeText(content)
}
fun getChapterCount(book: Book): Int {
return FileHelp.getFolder(getBookFolder(book)).list()?.size ?: 0
}
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
val filePath = getChapterPath(book, bookChapter)
runCatching {
@ -74,11 +85,15 @@ object BookHelp {
}
}
private fun getChapterPath(book: Book, bookChapter: BookChapter): String {
private fun getBookFolder(book: Book): String {
val bookFolder = formatFolderName(book.name + book.bookUrl)
return "${getBookCachePath()}${File.separator}$bookFolder"
}
private fun getChapterPath(book: Book, bookChapter: BookChapter): String {
val chapterFile =
String.format("%05d-%s", bookChapter.index, MD5Utils.md5Encode(bookChapter.title))
return "${getBookCachePath()}${File.separator}$bookFolder${File.separator}$chapterFile.nb"
return "${getBookFolder(book)}${File.separator}$chapterFile.nb"
}
private fun formatFolderName(folderName: String): String {

@ -27,8 +27,9 @@ object Backup {
legadoPath + File.separator + "Export"
}
private fun pbackup(path :String = legadoPath){
private fun pBackup(path: String = legadoPath) {
backupBookshelf(path)
backupBookGroup(path)
backupBookSource(path)
backupRssSource(path)
backupReplaceRule(path)
@ -39,7 +40,7 @@ object Backup {
fun backup() {
doAsync {
pbackup()
pBackup()
uiThread {
App.INSTANCE.toast(R.string.backup_success)
}
@ -48,7 +49,7 @@ object Backup {
fun autoBackup() {
doAsync {
pbackup()
pBackup()
}
}
@ -56,13 +57,22 @@ object Backup {
App.db.bookDao().allBooks.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "bookshelf.json")
file.writeText(json)
}
}
}
private fun backupBookGroup(path: String) {
App.db.bookGroupDao().all().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "bookGroup.json")
file.writeText(json)
}
}
}
private fun backupBookSource(path: String) {
App.db.bookSourceDao().all.let {
if (it.isNotEmpty()) {

@ -5,11 +5,7 @@ import io.legado.app.constant.BookType
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.*
import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.readInt
import io.legado.app.utils.readString
import io.legado.app.utils.readBool
import io.legado.app.utils.*
import java.util.regex.Pattern
object OldRule {
@ -129,7 +125,11 @@ object OldRule {
newRule = newRule.replace("|", "||")
}
}
if (newRule.contains("&") && !newRule.contains("&&")) {
if (newRule.contains("&")
&& !newRule.contains("&&")
&& !newRule.contains("http")
&& !newRule.startsWith("/")
) {
newRule = newRule.replace("&", "&&")
}
}
@ -145,10 +145,9 @@ object OldRule {
return toNewUrl(oldUrls)
val urls = oldUrls.split("(&&|\n)+".toRegex())
val newUrls = urls.map {
toNewUrl(it)?.replace("\n\\s*".toRegex(),"")
return urls.map {
toNewUrl(it)?.replace("\n\\s*".toRegex(), "")
}.joinToString("\n")
return newUrls
}
private fun toNewUrl(oldUrl: String?): String? {

@ -9,10 +9,7 @@ import com.jayway.jsonpath.ParseContext
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.data.entities.RssSource
import io.legado.app.data.entities.*
import io.legado.app.help.FileHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.*
@ -47,6 +44,15 @@ object Restore {
} catch (e: Exception) {
e.printStackTrace()
}
try {
val file = FileHelp.getFile(path + File.separator + "bookGroup.json")
val json = file.readText()
GSON.fromJsonArray<BookGroup>(json)?.let {
App.db.bookGroupDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
val file = FileHelp.getFile(path + File.separator + "bookSource.json")
val json = file.readText()

@ -206,6 +206,10 @@ object ATH {
.setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create()
itemIconTintList = colorStateList
itemTextColor = colorStateList
itemBackgroundResource = when(context.isNightTheme) {
true -> R.drawable.item_bg_dark
false -> R.drawable.item_bg_light
}
}
}

@ -16,7 +16,7 @@ object AnalyzeByRegex {
// 新建容器
val info = arrayListOf<String>()
for (groupIndex in 0..resM.groupCount()) {
info.add(resM.group(groupIndex))
info.add(resM.group(groupIndex)!!)
}
info
} else {
@ -43,7 +43,7 @@ object AnalyzeByRegex {
// 新建容器
val info = arrayListOf<String>()
for (groupIndex in 0..resM.groupCount()) {
info.add(resM.group(groupIndex))
info.add(resM.group(groupIndex)!!)
}
books.add(info)
} while (resM.find())

@ -320,7 +320,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
val pattern = Pattern.compile(rule.replaceRegex)
val matcher = pattern.matcher(vResult)
if (matcher.find()) {
matcher.group(0).replaceFirst(rule.replaceRegex.toRegex(), rule.replacement)
matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement)
} else {
""
}
@ -614,7 +614,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
val pattern = Pattern.compile("(第)(.+?)(章)")
val matcher = pattern.matcher(s)
return if (matcher.find()) {
matcher.group(1) + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3)
matcher.group(1)!! + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3)
} else {
s
}

@ -117,7 +117,7 @@ class AnalyzeUrl(
page?.let {
val matcher = pagePattern.matcher(ruleUrl)
while (matcher.find()) {
val pages = matcher.group(1).split(",")
val pages = matcher.group(1)!!.split(",")
ruleUrl = if (page <= pages.size) {
ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' })
} else {

@ -122,9 +122,9 @@ object RssParser {
if (matcherImg.find()) {
val imgTag = matcherImg.group(1)
val patternLink = "src\\s*=\\s*\"(.+?)\"".toPattern()
val matcherLink = patternLink.matcher(imgTag)
val matcherLink = patternLink.matcher(imgTag!!)
if (matcherLink.find()) {
url = matcherLink.group(1).trim()
url = matcherLink.group(1)!!.trim()
}
}
return url

@ -9,6 +9,7 @@ import android.graphics.BitmapFactory
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.support.v4.media.session.MediaSessionCompat
@ -25,6 +26,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.service.help.AudioPlay
import io.legado.app.ui.audio.AudioPlayActivity
@ -44,10 +46,10 @@ class AudioPlayService : BaseService(),
companion object {
var isRun = false
var pause = false
var timeMinute: Int = 0
}
var pause = false
private val handler = Handler()
private lateinit var audioManager: AudioManager
private var mFocusRequest: AudioFocusRequest? = null
@ -91,7 +93,6 @@ class AudioPlayService : BaseService(),
Action.prev -> moveToPrev()
Action.next -> moveToNext()
Action.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
Action.moveTo -> moveTo(intent.getIntExtra("index", AudioPlay.durChapterIndex))
Action.addTimer -> addTimer()
Action.setTimer -> setTimer(intent.getIntExtra("minute", 0))
Action.adjustProgress -> adjustProgress(intent.getIntExtra("position", position))
@ -121,7 +122,10 @@ class AudioPlayService : BaseService(),
AudioPlay.status = Status.PLAY
postEvent(Bus.AUDIO_STATE, Status.PLAY)
mediaPlayer.reset()
mediaPlayer.setDataSource(url)
val analyzeUrl =
AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true)
val uri = Uri.parse(analyzeUrl.url)
mediaPlayer.setDataSource(this, uri, analyzeUrl.headerMap)
mediaPlayer.prepareAsync()
} catch (e: Exception) {
launch {
@ -133,14 +137,18 @@ class AudioPlayService : BaseService(),
}
private fun pause(pause: Boolean) {
this.pause = pause
handler.removeCallbacks(mpRunnable)
position = mediaPlayer.currentPosition
mediaPlayer.pause()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
AudioPlay.status = Status.PAUSE
postEvent(Bus.AUDIO_STATE, Status.PAUSE)
upNotification()
if (url.contains(".m3u8", false)) {
stopSelf()
} else {
AudioPlayService.pause = pause
handler.removeCallbacks(mpRunnable)
position = mediaPlayer.currentPosition
mediaPlayer.pause()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
AudioPlay.status = Status.PAUSE
postEvent(Bus.AUDIO_STATE, Status.PAUSE)
upNotification()
}
}
private fun resume() {
@ -181,26 +189,24 @@ class AudioPlayService : BaseService(),
*/
override fun onPrepared(mp: MediaPlayer?) {
if (pause) return
mp?.let {
mp.start()
mp.seekTo(position)
postEvent(Bus.AUDIO_SIZE, mp.duration)
bookChapter?.let {
it.end = mp.duration.toLong()
}
handler.removeCallbacks(mpRunnable)
handler.post(mpRunnable)
mediaPlayer.start()
mediaPlayer.seekTo(position)
postEvent(Bus.AUDIO_SIZE, mediaPlayer.duration)
bookChapter?.let {
it.end = mediaPlayer.duration.toLong()
}
handler.removeCallbacks(mpRunnable)
handler.post(mpRunnable)
}
/**
* 播放出错
*/
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
AudioPlay.status = Status.STOP
postEvent(Bus.AUDIO_STATE, Status.STOP)
launch {
toast("error: $what $extra $url")
if (!mediaPlayer.isPlaying) {
AudioPlay.status = Status.STOP
postEvent(Bus.AUDIO_STATE, Status.STOP)
launch { toast("error: $what $extra $url") }
}
return true
}
@ -258,14 +264,14 @@ class AudioPlayService : BaseService(),
postEvent(Bus.AUDIO_SIZE, chapter.end?.toInt() ?: 0)
postEvent(Bus.AUDIO_PROGRESS, position)
}
download(chapter)
loadContent(chapter)
} ?: removeLoading(index)
}
}
}
}
private fun download(chapter: BookChapter) {
private fun loadContent(chapter: BookChapter) {
AudioPlay.book?.let { book ->
AudioPlay.webBook?.getContent(book, chapter, scope = this)
?.onSuccess(IO) { content ->
@ -311,16 +317,6 @@ class AudioPlayService : BaseService(),
}
}
private fun moveTo(index: Int) {
mediaPlayer.pause()
AudioPlay.durChapterIndex = index
AudioPlay.durPageIndex = 0
AudioPlay.book?.durChapterIndex = AudioPlay.durChapterIndex
saveRead()
position = 0
loadContent(AudioPlay.durChapterIndex)
}
private fun moveToPrev() {
if (AudioPlay.durChapterIndex > 0) {
mediaPlayer.pause()
@ -354,6 +350,7 @@ class AudioPlayService : BaseService(),
book.durChapterTime = System.currentTimeMillis()
book.durChapterIndex = AudioPlay.durChapterIndex
book.durChapterPos = AudioPlay.durPageIndex
book.durChapterTitle = subtitle
App.db.bookDao().update(book)
}
}

@ -26,7 +26,7 @@ import io.legado.app.help.MediaHelp
import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.page.TextChapter
import io.legado.app.ui.book.read.page.TextChapter
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent

@ -1,16 +1,19 @@
package io.legado.app.service
import android.content.Intent
import android.os.Handler
import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.Action
import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
@ -18,10 +21,26 @@ import java.util.concurrent.Executors
class DownloadService : BaseService() {
private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher()
private var tasks: ArrayList<Coroutine<*>> = arrayListOf()
private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() }
private var notificationContent = "正在启动下载"
private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download)
.setOngoing(true)
.setContentTitle(getString(R.string.download_offline))
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<DownloadService>(this, Action.stop)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
}
override fun onCreate() {
super.onCreate()
updateNotification("正在启动下载")
updateNotification(notificationContent)
handler.postDelayed(runnable, 1000)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -32,16 +51,17 @@ class DownloadService : BaseService() {
intent.getIntExtra("start", 0),
intent.getIntExtra("end", 0)
)
Action.stop -> stopSelf()
Action.stop -> stopDownload()
}
}
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
tasks.clear()
searchPool.close()
handler.removeCallbacks(runnable)
super.onDestroy()
}
private fun download(bookUrl: String?, start: Int, end: Int) {
@ -55,7 +75,7 @@ class DownloadService : BaseService() {
if (!BookHelp.hasContent(book, chapter)) {
webBook.getContent(book, chapter, scope = this, context = searchPool)
.onStart {
updateNotification(chapter.title)
notificationContent = chapter.title
}
.onSuccess(IO) { content ->
content?.let {
@ -75,21 +95,24 @@ class DownloadService : BaseService() {
}
}
private fun stopDownload() {
tasks.clear()
stopSelf()
}
private fun upDownload() {
updateNotification(notificationContent)
postEvent(Bus.UP_DOWNLOAD, true)
handler.removeCallbacks(runnable)
handler.postDelayed(runnable, 1000)
}
/**
* 更新通知
*/
private fun updateNotification(content: String) {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download)
.setOngoing(true)
.setContentTitle(getString(R.string.download_offline))
.setContentText(content)
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<DownloadService>(this, Action.stop)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val builder = notificationBuilder
builder.setContentText(content)
val notification = builder.build()
startForeground(AppConst.notificationIdDownload, notification)
}

@ -8,9 +8,11 @@ import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.Action
import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.help.IntentHelp
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent
import io.legado.app.web.HttpServer
import io.legado.app.web.WebSocketServer
import kotlinx.coroutines.launch
@ -54,6 +56,7 @@ class WebService : BaseService() {
if (webSocketServer?.isAlive == true) {
webSocketServer?.stop()
}
postEvent(Bus.WEB_SERVICE_STOP, true)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

@ -21,6 +21,10 @@ object AudioPlay {
var webBook: WebBook? = null
val loadingChapters = arrayListOf<Int>()
fun headers(): Map<String, String>? {
return webBook?.bookSource?.getHeaderMap()
}
fun play(context: Context) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.play
@ -85,12 +89,4 @@ object AudioPlay {
}
}
fun moveTo(context: Context, index: Int) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.moveTo
intent.putExtra("index", index)
context.startService(intent)
}
}
}

@ -11,8 +11,8 @@ import io.legado.app.help.IntentDataHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.widget.page.ChapterProvider
import io.legado.app.ui.widget.page.TextChapter
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.TextChapter
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main

@ -13,7 +13,6 @@ import io.legado.app.base.BaseActivity
import io.legado.app.lib.theme.ATH
import io.legado.app.utils.ACache
import kotlinx.android.synthetic.main.activity_donate.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.toast
import java.net.URLEncoder
@ -25,7 +24,6 @@ import java.net.URLEncoder
class DonateActivity : BaseActivity(R.layout.activity_donate) {
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
ATH.applyEdgeEffectColor(scroll_view)
vw_zfb_tz.setOnClickListener { aliDonate(this) }
cv_wx_gzh.setOnClickListener {
@ -78,8 +76,8 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) {
private fun aliDonate(context: Context) {
try {
val qrCode = URLEncoder.encode("tsx06677nwdk3javroq4ef0", "utf-8")
val aliPayQr = "alipayqr://platformapi/startapp?" +
"saId=10000007&qrcode=https://qr.alipay.com/$qrCode"
val aliPayQr =
"alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/$qrCode"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(aliPayQr))
context.startActivity(intent)
} catch (e: Exception) {

@ -10,30 +10,33 @@ import android.view.MenuItem
import android.widget.SeekBar
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus
import io.legado.app.constant.Status
import io.legado.app.constant.Theme
import io.legado.app.data.entities.Book
import io.legado.app.help.BlurTransformation
import io.legado.app.help.ImageLoader
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.service.AudioPlayService
import io.legado.app.service.help.AudioPlay
import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_audio_play.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.apache.commons.lang3.time.DateFormatUtils
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
import org.jetbrains.anko.startActivityForResult
class AudioPlayActivity : VMBaseActivity<AudioPlayViewModel>(R.layout.activity_audio_play),
class AudioPlayActivity :
VMBaseActivity<AudioPlayViewModel>(R.layout.activity_audio_play, theme = Theme.Dark),
ChangeSourceDialog.CallBack {
override val viewModel: AudioPlayViewModel
@ -43,7 +46,7 @@ class AudioPlayActivity : VMBaseActivity<AudioPlayViewModel>(R.layout.activity_a
private var adjustProgress = false
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
title_bar.background.alpha = 0
AudioPlay.titleData.observe(this, Observer { title_bar.title = it })
AudioPlay.coverData.observe(this, Observer { upCover(it) })
viewModel.initData(intent)
@ -119,6 +122,7 @@ class AudioPlayActivity : VMBaseActivity<AudioPlayViewModel>(R.layout.activity_a
.centerCrop()
.into(iv_cover)
ImageLoader.load(this, path)
.transition(DrawableTransitionOptions.withCrossFade(1500))
.thumbnail(defaultCover())
.centerCrop()
.apply(RequestOptions.bitmapTransform(BlurTransformation(this, 25)))
@ -172,7 +176,16 @@ class AudioPlayActivity : VMBaseActivity<AudioPlayViewModel>(R.layout.activity_a
when (requestCode) {
requestCodeChapter -> data?.getIntExtra("index", AudioPlay.durChapterIndex)?.let {
if (it != AudioPlay.durChapterIndex) {
AudioPlay.moveTo(this, it)
val isPlay = !AudioPlayService.pause
AudioPlay.pause(this)
AudioPlay.status = Status.STOP
AudioPlay.durChapterIndex = it
AudioPlay.durPageIndex = 0
AudioPlay.book?.durChapterIndex = AudioPlay.durChapterIndex
viewModel.saveRead()
if (isPlay) {
AudioPlay.play(this)
}
}
}
}

@ -124,13 +124,16 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
}
}
private fun saveRead() {
fun saveRead() {
execute {
AudioPlay.book?.let { book ->
book.lastCheckCount = 0
book.durChapterTime = System.currentTimeMillis()
book.durChapterIndex = AudioPlay.durChapterIndex
book.durChapterPos = AudioPlay.durPageIndex
App.db.bookChapterDao().getChapter(book.bookUrl, book.durChapterIndex)?.let {
book.durChapterTitle = it.title
}
App.db.bookDao().update(book)
}
}

@ -2,16 +2,22 @@ package io.legado.app.ui.book.info
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions.bitmapTransform
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.BookType
import io.legado.app.constant.Theme
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.BlurTransformation
import io.legado.app.help.ImageLoader
import io.legado.app.help.IntentDataHelp
import io.legado.app.ui.audio.AudioPlayActivity
@ -24,13 +30,13 @@ import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.activity_book_info.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_book_info),
class BookInfoActivity :
VMBaseActivity<BookInfoViewModel>(R.layout.activity_book_info, theme = Theme.Dark),
GroupSelectDialog.CallBack,
ChapterListAdapter.CallBack,
ChangeSourceDialog.CallBack {
@ -39,7 +45,7 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
get() = getViewModel(BookInfoViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
title_bar.background.alpha = 0
viewModel.bookData.observe(this, Observer { showBook(it) })
viewModel.isLoadingData.observe(this, Observer { upLoading(it) })
viewModel.chapterListData.observe(this, Observer { showChapter(it) })
@ -91,6 +97,11 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
return super.onMenuOpened(featureId, menu)
}
private fun defaultCover(): RequestBuilder<Drawable> {
return ImageLoader.load(this, R.drawable.image_cover_default)
.apply(bitmapTransform(BlurTransformation(this, 25)))
}
private fun showBook(book: Book) {
tv_name.text = book.name
tv_author.text = getString(R.string.author_show, book.author)
@ -100,9 +111,14 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
tv_intro.text = book.getDisplayIntro()
book.getDisplayCover()?.let {
ImageLoader.load(this, it)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
ImageLoader.load(this, it)
.transition(DrawableTransitionOptions.withCrossFade(1500))
.thumbnail(defaultCover())
.centerCrop()
.apply(bitmapTransform(BlurTransformation(this, 25)))
.into(bg_book) //模糊、渐变、缩小效果
}
val kinds = book.getKindList()
if (kinds.isEmpty()) {

@ -21,6 +21,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
@ -76,7 +77,7 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initData() {
tool_bar.title = getString(R.string.group_select)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.findItem(R.id.menu_group_local).isVisible = false
tool_bar.menu.findItem(R.id.menu_group_audio).isVisible = false

@ -1,5 +1,6 @@
package io.legado.app.ui.book.read
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
@ -8,6 +9,7 @@ import android.text.SpannableStringBuilder
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
@ -18,29 +20,28 @@ import io.legado.app.constant.Status
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.dialogs.*
import io.legado.app.receiver.TimeElectricityReceiver
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.Download
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.config.*
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.delegate.PageDelegate
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity
import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.ui.replacerule.edit.ReplaceEditDialog
import io.legado.app.ui.widget.page.ChapterProvider
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.widget.page.delegate.PageDelegate
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.dialog_download_choice.view.*
import kotlinx.android.synthetic.main.view_book_page.*
import kotlinx.android.synthetic.main.view_read_menu.*
import kotlinx.android.synthetic.main.view_title_bar.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
@ -59,6 +60,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override val viewModel: ReadBookViewModel
get() = getViewModel(ReadBookViewModel::class.java)
override val isInitFinish: Boolean
get() = viewModel.isInitFinish
private val requestCodeChapterList = 568
private val requestCodeEditSource = 111
private val requestCodeReplace = 31242
@ -66,7 +70,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onActivityCreated(savedInstanceState: Bundle?) {
Help.upLayoutInDisplayCutoutMode(window)
setSupportActionBar(toolbar)
initView()
ReadBook.callBack = this
ReadBook.titleDate.observe(this, Observer { title_bar.title = it })
@ -133,6 +136,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/**
* 菜单
*/
@SuppressLint("InflateParams")
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_change_source -> {
@ -148,6 +152,32 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
viewModel.refreshContent(it)
}
}
R.id.menu_download -> ReadBook.book?.let { book ->
alert(titleResource = R.string.download_offline) {
var view: View? = null
customView {
layoutInflater.inflate(R.layout.dialog_download_choice, null).apply {
view = this
edit_start.setText(book.durChapterIndex.toString())
edit_end.setText(book.totalChapterNum.toString())
}
}
yesButton {
view?.apply {
val start = edit_start?.text?.toString()?.toInt() ?: 0
val end = edit_end?.text?.toString()?.toInt() ?: book.totalChapterNum
Download.start(this@ReadBookActivity, book.bookUrl, start, end)
}
}
noButton()
}.show().applyTint()
}
R.id.menu_add_bookmark -> {
}
R.id.menu_copy_text -> {
}
}
return super.onCompatOptionsItemSelected(item)
}
@ -256,7 +286,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
}
override fun upContent(position: Int) {
page_view.upContent(position)
launch {
page_view.upContent(position)
}
}
override fun upView() {

@ -19,6 +19,8 @@ import kotlinx.coroutines.withContext
class ReadBookViewModel(application: Application) : BaseViewModel(application) {
var isInitFinish = false
fun initData(intent: Intent) {
execute {
ReadBook.inBookshelf = intent.getBooleanExtra("inBookshelf", true)
@ -37,25 +39,27 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
private fun initBook(book: Book) {
if (ReadBook.book?.bookUrl != book.bookUrl) {
ReadBook.resetData(book)
val count = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (count == 0) {
isInitFinish = true
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) {
if (book.tocUrl.isEmpty()) {
loadBookInfo(book)
} else {
loadChapterList(book)
}
} else {
if (ReadBook.durChapterIndex > count - 1) {
ReadBook.durChapterIndex = count - 1
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
}
ReadBook.chapterSize = count
ReadBook.loadContent()
}
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
}
} else {
isInitFinish = true
ReadBook.titleDate.postValue(book.name)
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) {
if (book.tocUrl.isEmpty()) {
loadBookInfo(book)

@ -11,6 +11,7 @@ import androidx.core.view.get
import androidx.fragment.app.DialogFragment
import io.legado.app.R
import io.legado.app.constant.Bus
import io.legado.app.constant.PreferKey
import io.legado.app.help.ImageLoader
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.selector
@ -66,7 +67,7 @@ class ReadStyleDialog : DialogFragment() {
}
private fun initData() {
requireContext().getPrefInt("pageAnim").let {
requireContext().getPrefInt(PreferKey.pageAnim).let {
if (it >= 0 && it < rg_page_anim.childCount) {
rg_page_anim.check(rg_page_anim[it].id)
}
@ -86,13 +87,13 @@ class ReadStyleDialog : DialogFragment() {
}
tv_text_font.onClick {
FontSelectDialog(requireContext()).apply {
curPath = requireContext().getPrefString("readBookFont")
curPath = requireContext().getPrefString(PreferKey.readBookFont)
defaultFont = {
requireContext().putPrefString("readBookFont", "")
requireContext().putPrefString(PreferKey.readBookFont, "")
postEvent(Bus.UP_CONFIG, true)
}
selectFile = {
requireContext().putPrefString("readBookFont", it)
requireContext().putPrefString(PreferKey.readBookFont, it)
postEvent(Bus.UP_CONFIG, true)
}
}.show()
@ -179,7 +180,7 @@ class ReadStyleDialog : DialogFragment() {
rg_page_anim.onCheckedChange { _, checkedId ->
for (i in 0 until rg_page_anim.childCount) {
if (checkedId == rg_page_anim[i].id) {
requireContext().putPrefInt("pageAnim", i)
requireContext().putPrefInt(PreferKey.pageAnim, i)
val activity = activity
if (activity is ReadBookActivity) {
activity.page_view.upPageAnim(i)

@ -1,9 +1,11 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import androidx.core.text.HtmlCompat
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT
import io.legado.app.App
import io.legado.app.data.entities.BookChapter
import io.legado.app.lib.theme.accentColor
@ -17,7 +19,9 @@ object ChapterProvider {
fun getTextChapter(
bookChapter: BookChapter,
content: String, chapterSize: Int
content: String,
chapterSize: Int,
isHtml: Boolean = false
): TextChapter {
textView?.let {
val textPages = arrayListOf<TextPage>()
@ -26,7 +30,15 @@ object ChapterProvider {
var surplusText = content
var pageIndex = 0
while (surplusText.isNotEmpty()) {
val spannableStringBuilder = SpannableStringBuilder(surplusText)
val spannableStringBuilder =
if (isHtml) {
HtmlCompat.fromHtml(
surplusText,
FROM_HTML_MODE_COMPACT
) as SpannableStringBuilder
} else {
SpannableStringBuilder(surplusText)
}
if (pageIndex == 0) {
val end = surplusText.indexOf("\n")
if (end > 0) {

@ -1,14 +1,15 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import io.legado.app.R
import io.legado.app.constant.Bus
import io.legado.app.utils.postEvent
class ContentSelectActionCallback(private val textView: ContentTextView) : ActionMode.Callback {
class ContentSelectActionCallback(private val textView: TextView) : ActionMode.Callback {
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when (item?.itemId) {

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint
import android.content.Context
@ -241,4 +241,5 @@ class ContentTextView : AppCompatTextView {
}
}
}
}

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint
import android.content.Context
@ -81,7 +81,7 @@ class ContentView : FrameLayout {
tv_bottom_right.setTextColor(it)
}
}
context.getPrefString("readBookFont")?.let {
context.getPrefString(PreferKey.readBookFont)?.let {
if (it.isNotEmpty()) {
content_text_view.typeface = Typeface.createFromFile(it)
} else {
@ -133,7 +133,9 @@ class ContentView : FrameLayout {
fun scrollTo(pos: Int?) {
if (pos != null) {
content_text_view.post {
content_text_view.scrollTo(0, content_text_view.layout.getLineTop(pos))
if (content_text_view.layout.lineCount >= pos) {
content_text_view.scrollTo(0, content_text_view.layout.getLineTop(pos))
}
}
}
}

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
interface DataSource {
val isScrollDelegate: Boolean
@ -9,8 +9,6 @@ interface DataSource {
fun getChapterPosition(): Int
fun getChapter(position: Int): TextChapter?
fun getCurrentChapter(): TextChapter?
fun getNextChapter(): TextChapter?

@ -1,9 +1,7 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
abstract class PageFactory<DATA>(protected val dataSource: DataSource) {
abstract fun pageAt(index: Int): DATA?
abstract fun moveToFirst()
abstract fun moveToLast()

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint
import android.content.Context
@ -6,11 +6,12 @@ import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.widget.page.curl.CurlView
import io.legado.app.ui.widget.page.delegate.*
import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.utils.activity
import io.legado.app.utils.getPrefInt
class PageView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs),
@ -24,7 +25,6 @@ class PageView(context: Context, attrs: AttributeSet) :
var prevPage: ContentView? = null
var curPage: ContentView? = null
var nextPage: ContentView? = null
var curlView: CurlView? = null
init {
callBack = activity as? CallBack
@ -37,7 +37,7 @@ class PageView(context: Context, attrs: AttributeSet) :
upBg()
setWillNotDraw(false)
pageFactory = TextPageFactory(this)
upPageAnim()
upPageAnim(context.getPrefInt(PreferKey.pageAnim))
curPage?.callBack = this
}
@ -88,11 +88,7 @@ class PageView(context: Context, attrs: AttributeSet) :
}
}
fun upPageAnim(pageAnim: Int = 0) {
if (curlView != null) {
removeView(curlView)
curlView = null
}
fun upPageAnim(pageAnim: Int) {
pageDelegate = null
pageDelegate = when (pageAnim) {
0 -> CoverPageDelegate(this)
@ -190,20 +186,16 @@ class PageView(context: Context, attrs: AttributeSet) :
return ReadBook.durChapterIndex
}
override fun getChapter(position: Int): TextChapter? {
return ReadBook.textChapter(position)
}
override fun getCurrentChapter(): TextChapter? {
return ReadBook.textChapter(0)
return if (callBack?.isInitFinish == true) ReadBook.textChapter(0) else null
}
override fun getNextChapter(): TextChapter? {
return ReadBook.textChapter(1)
return if (callBack?.isInitFinish == true) ReadBook.textChapter(1) else null
}
override fun getPreviousChapter(): TextChapter? {
return ReadBook.textChapter(-1)
return if (callBack?.isInitFinish == true) ReadBook.textChapter(-1) else null
}
override fun hasNextChapter(): Boolean {
@ -248,5 +240,6 @@ class PageView(context: Context, attrs: AttributeSet) :
*/
fun clickCenter()
val isInitFinish: Boolean
}
}

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import android.text.SpannableStringBuilder
import kotlin.math.min
@ -54,7 +54,7 @@ data class TextChapter(
fun getReadLength(pageIndex: Int): Int {
var length = 0
val maxIndex = min(pageIndex, pageLines.lastIndex)
val maxIndex = min(pageIndex, pageLines.size)
for (index in 0 until maxIndex) {
length += pageLengths[index]
}

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import android.text.Spannable
import android.text.SpannableStringBuilder

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page
package io.legado.app.ui.book.read.page
import io.legado.app.service.help.ReadBook
@ -21,11 +21,6 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
}
}
override fun pageAt(index: Int): TextPage {
return dataSource.getCurrentChapter()?.page(index)
?: TextPage(index = index, title = "index:$index")
}
override fun moveToFirst() {
dataSource.setPageIndex(0)
}

@ -1,9 +1,9 @@
package io.legado.app.ui.widget.page.delegate
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.drawable.GradientDrawable
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.book.read.page.PageView
class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page.delegate
package io.legado.app.ui.book.read.page.delegate
import android.view.MotionEvent

@ -1,7 +1,7 @@
package io.legado.app.ui.widget.page.delegate
package io.legado.app.ui.book.read.page.delegate
import android.view.MotionEvent
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.screenshot
import kotlin.math.abs

@ -1,7 +1,7 @@
package io.legado.app.ui.widget.page.delegate
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.book.read.page.PageView
class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
override fun onScrollStart() {

@ -1,4 +1,4 @@
package io.legado.app.ui.widget.page.delegate
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Bitmap
import android.graphics.Canvas
@ -9,8 +9,8 @@ import android.widget.Scroller
import androidx.annotation.CallSuper
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import com.google.android.material.snackbar.Snackbar
import io.legado.app.ui.widget.page.ContentView
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.screenshot
import io.legado.app.utils.snackbar
import kotlin.math.abs
@ -69,7 +69,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
var isRunning = false
var isStarted = false
protected fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
open fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
startX = x
startY = y
@ -78,7 +78,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
}
}
protected fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) {
open fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) {
touchX = x
touchY = y

@ -1,9 +1,9 @@
package io.legado.app.ui.widget.page.delegate
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import android.graphics.Matrix
import android.view.MotionEvent
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.screenshot
import kotlin.math.abs

@ -0,0 +1,526 @@
package io.legado.app.ui.book.read.page.delegate
import android.graphics.*
import android.graphics.drawable.GradientDrawable
import android.os.Build
import io.legado.app.ui.book.read.page.PageView
import kotlin.math.*
class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
private var mCornerX = 1 // 拖拽点对应的页脚
private var mCornerY = 1
private val mPath0: Path = Path()
private val mPath1: Path = Path()
// 贝塞尔曲线起始点
private val mBezierStart1 = PointF()
// 贝塞尔曲线控制点
private val mBezierControl1 = PointF()
// 贝塞尔曲线顶点
private val mBezierVertex1 = PointF()
// 贝塞尔曲线结束点
private var mBezierEnd1 = PointF()
// 另一条贝塞尔曲线
private val mBezierStart2 = PointF()
private val mBezierControl2 = PointF()
private val mBezierVertex2 = PointF()
private var mBezierEnd2 = PointF()
private var mMiddleX = 0f
private var mMiddleY = 0f
private var mDegrees = 0f
private var mTouchToCornerDis = 0f
private var mColorMatrixFilter: ColorMatrixColorFilter? = null
private val mMatrix: Matrix = Matrix()
private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f)
// 是否属于右上左下
private var mIsRT_LB = false
private var mMaxLength = 0f
// 背面颜色组
private var mBackShadowColors: IntArray? = null
// 前面颜色组
private var mFrontShadowColors: IntArray? = null
// 有阴影的GradientDrawable
private var mBackShadowDrawableLR: GradientDrawable? = null
private var mBackShadowDrawableRL: GradientDrawable? = null
private var mFolderShadowDrawableLR: GradientDrawable? = null
private var mFolderShadowDrawableRL: GradientDrawable? = null
private var mFrontShadowDrawableHBT: GradientDrawable? = null
private var mFrontShadowDrawableHTB: GradientDrawable? = null
private var mFrontShadowDrawableVLR: GradientDrawable? = null
private var mFrontShadowDrawableVRL: GradientDrawable? = null
private val mPaint: Paint = Paint()
init {
mMaxLength = hypot(pageView.width.toDouble(), pageView.height.toDouble()).toFloat()
mPaint.style = Paint.Style.FILL
//设置颜色数组
createDrawable()
val cm = ColorMatrix()
val array = floatArrayOf(
1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f,
0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f
)
cm.set(array)
mColorMatrixFilter = ColorMatrixColorFilter(cm)
}
override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) {
super.setStartPoint(x, y, invalidate)
calcCornerXY(x, y)
}
override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) {
super.setTouchPoint(x, y, invalidate)
//触摸y中间位置吧y变成屏幕高度
//触摸y中间位置吧y变成屏幕高度
if (startY > pageView.height / 3.0
&& startY < pageView.height * 2 / 3.0
|| direction == Direction.PREV
) {
touchY = pageView.height.toFloat()
}
if (startY > pageView.height / 3.0
&& startY < pageView.height / 2.0
&& direction == Direction.NEXT
) {
touchY = 1f
}
}
override fun onScrollStart() {
val distanceX: Float
when (direction) {
Direction.NEXT -> distanceX =
if (isCancel) {
var dis = viewWidth - startX + touchX
if (dis > viewWidth) {
dis = viewWidth.toFloat()
}
viewWidth - dis
} else {
-(touchX + (viewWidth - startX))
}
else -> distanceX =
if (isCancel) {
-(touchX - startX)
} else {
viewWidth - (touchX - startX)
}
}
startScroll(touchX.toInt(), 0, distanceX.toInt(), 0)
}
override fun onScrollStop() {
curPage?.x = 0.toFloat()
if (!isCancel) {
pageView.fillPage(direction)
}
}
override fun onDraw(canvas: Canvas) {
bitmap?.let {
if (direction === Direction.NEXT) {
calcPoints()
drawCurrentPageArea(canvas, it, mPath0!!) //绘制翻页时的正面页
// drawNextPageAreaAndShadow(canvas, it)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, it)
} else {
calcPoints()
drawCurrentPageArea(canvas, it, mPath0!!)
// drawNextPageAreaAndShadow(canvas, it)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, it)
}
}
}
/**
* 创建阴影的GradientDrawable
*/
private fun createDrawable() {
val color = intArrayOf(0x333333, -0x4fcccccd)
mFolderShadowDrawableRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, color
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFolderShadowDrawableLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, color
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111)
mBackShadowDrawableRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mBackShadowDrawableLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111)
mFrontShadowDrawableVLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableVRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableHTB = GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableHBT = GradientDrawable(
GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
}
/**
* 绘制翻起页背面
*/
private fun drawCurrentBackArea(
canvas: Canvas,
bitmap: Bitmap
) {
val i = (mBezierStart1.x + mBezierControl1.x).toInt() / 2
val f1 = abs(i - mBezierControl1.x)
val i1 = (mBezierStart2.y + mBezierControl2.y).toInt() / 2
val f2 = abs(i1 - mBezierControl2.y)
val f3 = min(f1, f2)
mPath1.reset()
mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y)
mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y)
mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y)
mPath1.close()
val mFolderShadowDrawable: GradientDrawable
val left: Int
val right: Int
if (mIsRT_LB) {
left = (mBezierStart1.x - 1).toInt()
right = (mBezierStart1.x + f3 + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableLR!!
} else {
left = (mBezierStart1.x - f3 - 1).toInt()
right = (mBezierStart1.x + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableRL!!
}
canvas.save()
try {
canvas.clipPath(mPath0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipPath(mPath1)
} else {
canvas.clipPath(mPath1, Region.Op.INTERSECT)
}
} catch (ignored: Exception) {
}
mPaint!!.colorFilter = mColorMatrixFilter
val dis = hypot(
mCornerX - mBezierControl1.x.toDouble(),
mBezierControl2.y - mCornerY.toDouble()
).toFloat()
val f8 = (mCornerX - mBezierControl1.x) / dis
val f9 = (mBezierControl2.y - mCornerY) / dis
mMatrixArray[0] = 1 - 2 * f9 * f9
mMatrixArray[1] = 2 * f8 * f9
mMatrixArray[3] = mMatrixArray[1]
mMatrixArray[4] = 1 - 2 * f8 * f8
mMatrix.reset()
mMatrix.setValues(mMatrixArray)
mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y)
mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y)
canvas.drawBitmap(bitmap, mMatrix, mPaint)
mPaint.colorFilter = null
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y)
mFolderShadowDrawable.setBounds(
left, mBezierStart1.y.toInt(), right,
(mBezierStart1.y + mMaxLength).toInt()
)
mFolderShadowDrawable.draw(canvas)
canvas.restore()
}
/**
* 绘制翻起页的阴影
*/
private fun drawCurrentPageShadow(canvas: Canvas) {
val degree: Double = if (mIsRT_LB) {
(Math.PI / 4 - atan2(mBezierControl1.y - touchX, touchY - mBezierControl1.x))
} else {
(Math.PI / 4 - atan2(touchY - mBezierControl1.y, touchX - mBezierControl1.x))
}
// 翻起页阴影顶点与touch点的距离
val d1 = 25.toFloat() * 1.414 * cos(degree)
val d2 = 25.toFloat() * 1.414 * sin(degree)
val x = (touchX + d1).toFloat()
val y: Float
y = if (mIsRT_LB) {
(touchY + d2).toFloat()
} else {
(touchY - d2).toFloat()
}
mPath1.reset()
mPath1.moveTo(x, y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierControl1.x, mBezierControl1.y)
mPath1.lineTo(mBezierStart1.x, mBezierStart1.y)
mPath1.close()
canvas.save()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath0)
} else {
canvas.clipPath(mPath0, Region.Op.XOR)
}
canvas.clipPath(mPath1, Region.Op.INTERSECT)
} catch (ignored: java.lang.Exception) {
}
var leftX: Int
var rightX: Int
var mCurrentPageShadow: GradientDrawable
if (mIsRT_LB) {
leftX = mBezierControl1.x.toInt()
rightX = mBezierControl1.x.toInt() + 25
mCurrentPageShadow = mFrontShadowDrawableVLR!!
} else {
leftX = (mBezierControl1.x - 25).toInt()
rightX = mBezierControl1.x.toInt() + 1
mCurrentPageShadow = mFrontShadowDrawableVRL!!
}
var rotateDegrees: Float =
Math.toDegrees(atan2(touchX - mBezierControl1.x, mBezierControl1.y - touchY).toDouble())
.toFloat()
canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y)
mCurrentPageShadow.setBounds(
leftX,
(mBezierControl1.y - mMaxLength).toInt(), rightX,
mBezierControl1.y.toInt()
)
mCurrentPageShadow.draw(canvas)
canvas.restore()
mPath1.reset()
mPath1.moveTo(x, y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierControl2.x, mBezierControl2.y)
mPath1.lineTo(mBezierStart2.x, mBezierStart2.y)
mPath1.close()
canvas.save()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath0)
} else {
canvas.clipPath(mPath0, Region.Op.XOR)
}
canvas.clipPath(mPath1)
} catch (ignored: java.lang.Exception) {
}
if (mIsRT_LB) {
leftX = mBezierControl2.y.toInt()
rightX = (mBezierControl2.y + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableHTB!!
} else {
leftX = (mBezierControl2.y - 25).toInt()
rightX = (mBezierControl2.y + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableHBT!!
}
rotateDegrees = Math.toDegrees(
atan2(mBezierControl2.y - touchY, mBezierControl2.x - touchX).toDouble()
).toFloat()
canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y)
val temp: Float =
if (mBezierControl2.y < 0) mBezierControl2.y - pageView.height else mBezierControl2.y
val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt()
if (hmg > mMaxLength) mCurrentPageShadow
.setBounds(
(mBezierControl2.x - 25).toInt() - hmg, leftX,
(mBezierControl2.x + mMaxLength).toInt() - hmg,
rightX
) else mCurrentPageShadow.setBounds(
(mBezierControl2.x - mMaxLength).toInt(), leftX,
mBezierControl2.x.toInt(), rightX
)
mCurrentPageShadow.draw(canvas)
canvas.restore()
}
private fun drawNextPageAreaAndShadow(
canvas: Canvas,
bitmap: Bitmap
) {
mPath1.reset()
mPath1.moveTo(mBezierStart1.x, mBezierStart1.y)
mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y)
mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y)
mPath1.lineTo(mBezierStart2.x, mBezierStart2.y)
mPath1.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath1.close()
mDegrees = Math.toDegrees(
atan2(
(mBezierControl1.x - mCornerX).toDouble(),
mBezierControl2.y - mCornerY.toDouble()
)
).toFloat()
val leftX: Int
val rightY: Int
val mBackShadowDrawable: GradientDrawable
if (mIsRT_LB) { //左下及右上
leftX = mBezierStart1.x.toInt()
rightY = (mBezierStart1.x + mTouchToCornerDis / 4).toInt()
mBackShadowDrawable = mBackShadowDrawableLR!!
} else {
leftX = (mBezierStart1.x - mTouchToCornerDis / 4).toInt()
rightY = mBezierStart1.x.toInt()
mBackShadowDrawable = mBackShadowDrawableRL!!
}
canvas.save()
try {
canvas.clipPath(mPath0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipPath(mPath1)
} else {
canvas.clipPath(mPath1, Region.Op.INTERSECT)
}
//canvas.clipPath(mPath1, Region.Op.INTERSECT);
} catch (ignored: java.lang.Exception) {
}
canvas.drawBitmap(bitmap, 0f, 0f, null)
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y)
mBackShadowDrawable.setBounds(
leftX, mBezierStart1.y.toInt(), rightY,
(mMaxLength + mBezierStart1.y).toInt()
) //左上及右下角的xy坐标值,构成一个矩形
mBackShadowDrawable.draw(canvas)
canvas.restore()
}
private fun drawCurrentPageArea(
canvas: Canvas,
bitmap: Bitmap,
path: Path
) {
mPath0.reset()
mPath0.moveTo(mBezierStart1.x, mBezierStart1.y)
mPath0.quadTo(
mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x,
mBezierEnd1.y
)
mPath0.lineTo(touchX, touchY)
mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y)
mPath0.quadTo(
mBezierControl2.x, mBezierControl2.y, mBezierStart2.x,
mBezierStart2.y
)
mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath0.close()
canvas.save()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(path)
} else {
canvas.clipPath(path, Region.Op.XOR)
}
canvas.drawBitmap(bitmap, 0f, 0f, null)
try {
canvas.restore()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
/**
* 计算拖拽点对应的拖拽脚
*/
private fun calcCornerXY(x: Float, y: Float) {
if (x <= pageView.width / 2.0) {
mCornerX = 0
} else {
mCornerX = pageView.width
}
if (y <= pageView.height / 2.0) {
mCornerY = 0
} else {
mCornerY = pageView.height
}
mIsRT_LB = (mCornerX == 0 && mCornerY == pageView.height
|| mCornerX == pageView.width && mCornerY == 0)
}
private fun calcPoints() {
mMiddleX = (touchX + mCornerX) / 2
mMiddleY = (touchY + mCornerY) / 2
mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat()
val f4 = mCornerY - mMiddleY
if (f4 == 0f) {
mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
}
mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2
mBezierStart1.y = mCornerY.toFloat()
// 当mBezierStart1.x < 0或者mBezierStart1.x > 480时
// 如果继续翻页,会出现BUG故在此限制
if (touchX > 0 && touchX < pageView.width) {
if (mBezierStart1.x < 0 || mBezierStart1.x > pageView.width) {
if (mBezierStart1.x < 0) mBezierStart1.x = pageView.width - mBezierStart1.x
val f1: Float = abs(mCornerX - touchX)
val f2: Float = pageView.width * f1 / mBezierStart1.x
touchX = abs(mCornerX - f2)
val f3: Float = abs(mCornerX - touchX) * abs(mCornerY - touchX) / f1
touchX = abs(mCornerY - f3)
mMiddleX = (touchX + mCornerX) / 2
mMiddleY = (touchY + mCornerY) / 2
mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat()
val f5 = mCornerY - mMiddleY
if (f5 == 0f) {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
}
mBezierStart1.x = (mBezierControl1.x
- (mCornerX - mBezierControl1.x) / 2)
}
}
mBezierStart2.x = mCornerX.toFloat()
mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2
mTouchToCornerDis = hypot(touchX - mCornerX, touchY - mCornerY)
mBezierEnd1 = getCross(
PointF(touchX, touchY), mBezierControl1, mBezierStart1,
mBezierStart2
)
mBezierEnd2 = getCross(
PointF(touchX, touchY), mBezierControl2, mBezierStart1,
mBezierStart2
)
mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4
mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4
mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4
mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4
}
/**
* 求解直线P1P2和直线P3P4的交点坐标
*/
private fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF {
val crossP = PointF()
// 二元函数通式: y=ax+b
val a1 = (P2.y - P1.y) / (P2.x - P1.x)
val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x)
val a2 = (P4.y - P3.y) / (P4.x - P3.x)
val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x)
crossP.x = (b2 - b1) / (a1 - a2)
crossP.y = a1 * crossP.x + b1
return crossP
}
}

@ -1,8 +1,8 @@
package io.legado.app.ui.widget.page.delegate
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import android.graphics.Matrix
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.book.read.page.PageView
class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {

@ -3,6 +3,8 @@ package io.legado.app.ui.book.search
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
@ -28,6 +30,7 @@ import kotlinx.coroutines.withContext
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity
class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_search),
SearchViewModel.CallBack,
BookAdapter.CallBack,
@ -110,10 +113,10 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
}
})
search_view.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
ll_history.visible()
if (!hasFocus && search_view.query.toString().trim().isEmpty()) {
finish()
} else {
ll_history.invisible()
openOrCloseHistory(hasFocus)
}
}
}
@ -175,6 +178,15 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
}
}
private fun openOrCloseHistory(open: Boolean) {
if (open) {
upHistory(search_view.query.toString())
ll_history.visibility = VISIBLE
} else {
ll_history.visibility = GONE
}
}
private fun upGroupMenu() {
val selectedGroup = getPrefString("searchGroup") ?: ""
menu?.removeGroup(R.id.source_group)
@ -216,7 +228,13 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
} else {
App.db.searchKeywordDao().liveDataSearch(key)
}
historyData?.observe(this, Observer { historyKeyAdapter.setItems(it) })
historyData?.observe(this, Observer { historyKeyAdapter.setItems(it)
if (it.isEmpty()) {
tv_clear_history.invisible()
} else {
tv_clear_history.visible()
}
})
}
override fun startSearch() {
@ -254,4 +272,4 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
}
}
}
}
}

@ -36,7 +36,6 @@ import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_source.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.view_search.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.startService
@ -57,7 +56,6 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
private var groupMenu: SubMenu? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
initUriScheme()
initRecyclerView()
initSearchView()

@ -91,7 +91,8 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
App.db.bookSourceDao().getBookSource(it)
}.let {
val json = GSON.toJson(it)
val file = FileHelp.getFile(Backup.exportPath + File.separator + "exportBookSource.json")
val file =
FileHelp.getFile(Backup.exportPath + File.separator + "exportBookSource.json")
file.writeText(json)
}
}.onSuccess {

@ -19,6 +19,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
@ -60,7 +61,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initData() {
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.group_manage)
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())

@ -121,6 +121,7 @@ class ChangeSourceDialog : DialogFragment(),
override fun changeTo(searchBook: SearchBook) {
val book = searchBook.toBook()
callBack?.oldBook?.let { oldBook ->
book.group = oldBook.group
book.durChapterIndex = oldBook.durChapterIndex
book.durChapterPos = oldBook.durChapterPos
book.durChapterTitle = oldBook.durChapterTitle

@ -17,7 +17,6 @@ import io.legado.app.utils.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.activity_chapter_list.*
import kotlinx.android.synthetic.main.view_tab_layout.*
import kotlinx.android.synthetic.main.view_title_bar.*
class ChapterListActivity : VMBaseActivity<ChapterListViewModel>(R.layout.activity_chapter_list) {
override val viewModel: ChapterListViewModel
@ -26,7 +25,6 @@ class ChapterListActivity : VMBaseActivity<ChapterListViewModel>(R.layout.activi
private var searchView: SearchView? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
tab_layout.setSelectedTabIndicatorColor(accentColor)
viewModel.bookUrl = intent.getStringExtra("bookUrl")
viewModel.loadBook {

@ -13,6 +13,7 @@ import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.utils.getViewModelOfActivity
import kotlinx.android.synthetic.main.fragment_chapter_list.*
import org.jetbrains.anko.sdk27.listeners.onClick
@ -48,9 +49,9 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
viewModel.bookUrl?.let { bookUrl ->
App.db.bookChapterDao().observeByBook(bookUrl).observe(viewLifecycleOwner, Observer {
adapter.setItems(it)
viewModel.book?.let {
durChapterIndex = it.durChapterIndex
tv_current_chapter_info.text = it.durChapterTitle
viewModel.book?.let { book ->
durChapterIndex = book.durChapterIndex
tv_current_chapter_info.text = book.durChapterTitle
recycler_view.scrollToPosition(durChapterIndex)
}
})
@ -58,6 +59,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
}
private fun initView() {
ll_chapter_base_info.setBackgroundColor(backgroundColor)
iv_chapter_top.onClick { recycler_view.scrollToPosition(0) }
iv_chapter_bottom.onClick {
if (adapter.itemCount > 0) {

@ -5,7 +5,6 @@ import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_config.*
import kotlinx.android.synthetic.main.view_title_bar.*
class ConfigActivity : VMBaseActivity<ConfigViewModel>(R.layout.activity_config) {
override val viewModel: ConfigViewModel
@ -15,7 +14,6 @@ class ConfigActivity : VMBaseActivity<ConfigViewModel>(R.layout.activity_config)
intent.getIntExtra("configType", -1).let {
if (it != -1) viewModel.configType = it
}
this.setSupportActionBar(toolbar)
when (viewModel.configType) {
ConfigViewModel.TYPE_CONFIG -> {

@ -20,8 +20,12 @@ import io.legado.app.utils.*
class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
val items = arrayOf("极简","曜夜","经典")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme)
findPreference<Preference>("defaultTheme")?.summary = "${items[getPrefInt("default_theme", 0)]}"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -96,23 +100,34 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
"defaultTheme" -> {
activity?.let {
AlertDialog.Builder(it)
.setTitle("恢复默认主题")
.setMessage("是否确认恢复?")
.setPositiveButton(R.string.ok) { _, _ ->
preferenceManager.sharedPreferences.edit()
// light mode
.putInt("colorPrimary", getCompatColor(R.color.md_light_blue_500))
.putInt("colorAccent", getCompatColor(R.color.md_pink_800))
.putInt("colorBackground", getCompatColor(R.color.md_grey_100))
// dark mode
.putInt("colorPrimaryNight", getCompatColor(R.color.md_grey_900))
.putInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
.putInt("colorBackgroundNight", getCompatColor(R.color.md_black_1000))
.apply()
App.INSTANCE.applyTheme()
.setTitle("切换默认主题")
.setItems(items){
_,which ->
preference.summary = "${items[which]}"
putPrefInt("default_theme", which)
when (which) {
0 -> {
putPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100))
putPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color))
putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))
putPrefBoolean("isNightTheme", false)
}
1 -> {
putPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color))
putPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color))
putPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
putPrefBoolean("isNightTheme", true)
}
2 -> {
putPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500))
putPrefInt("colorAccent", getCompatColor(R.color.md_pink_800))
putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))
putPrefBoolean("isNightTheme", false)
}
}
App.INSTANCE.applyDayNight()
recreateActivities()
}
.setNegativeButton(R.string.cancel, null)
.show().applyTint()
}
}

@ -0,0 +1,71 @@
package io.legado.app.ui.download
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.constant.Bus
import io.legado.app.data.entities.Book
import io.legado.app.service.help.Download
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.activity_download.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
class DownloadActivity : BaseActivity(R.layout.activity_download) {
lateinit var adapter: DownloadAdapter
private var bookshelfLiveData: LiveData<List<Book>>? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
initRecyclerView()
initLiveData()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.download, menu)
return super.onCompatCreateOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_download_start -> launch(IO) {
App.db.bookDao().webBooks.forEach { book ->
Download.start(
this@DownloadActivity,
book.bookUrl,
book.durChapterIndex,
book.totalChapterNum
)
}
}
}
return super.onCompatOptionsItemSelected(item)
}
private fun initRecyclerView() {
recycler_view.layoutManager = LinearLayoutManager(this)
adapter = DownloadAdapter(this)
recycler_view.adapter = adapter
}
private fun initLiveData() {
bookshelfLiveData?.removeObservers(this)
bookshelfLiveData = App.db.bookDao().observeDownload()
bookshelfLiveData?.observe(this, Observer {
adapter.setItems(it)
})
}
override fun observeLiveBus() {
observeEvent<Boolean>(Bus.UP_DOWNLOAD) {
adapter.notifyItemRangeChanged(0, adapter.itemCount, true)
}
}
}

@ -0,0 +1,36 @@
package io.legado.app.ui.download
import android.content.Context
import android.view.View
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp
import kotlinx.android.synthetic.main.item_download.view.*
class DownloadAdapter(context: Context) :
SimpleRecyclerAdapter<Book>(context, R.layout.item_download) {
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) {
if (payloads.isEmpty()) {
tv_name.text = item.name
tv_author.text = item.author
upDownloadCount(this, item)
} else {
upDownloadCount(this, item)
}
}
}
private fun upDownloadCount(view: View, book: Book) {
view.tv_download.text = context.getString(
R.string.download_count,
BookHelp.getChapterCount(book),
book.totalChapterNum
)
}
}

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.filechooser.adapter.FileAdapter
import io.legado.app.ui.filechooser.adapter.PathAdapter
@ -133,7 +134,7 @@ class FileChooserDialog : DialogFragment(),
tool_bar.menu.add(menuTitle)
}
}
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
}

@ -13,7 +13,6 @@ import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus
import io.legado.app.help.ActivityHelp
import io.legado.app.help.storage.Backup
import io.legado.app.lib.theme.ATH
import io.legado.app.service.BaseReadAloudService
@ -113,14 +112,10 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
}
override fun finish() {
if (ActivityHelp.size() > 1) {
moveTaskToBack(true)
} else {
if (!BuildConfig.DEBUG) {
Backup.autoBackup()
}
super.finish()
if (!BuildConfig.DEBUG) {
Backup.autoBackup()
}
super.finish()
}
override fun onDestroy() {

@ -19,6 +19,7 @@ import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.download.DownloadActivity
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getViewModel
import io.legado.app.utils.putPrefInt
@ -62,7 +63,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
}
R.id.menu_arrange_bookshelf -> {
}
R.id.menu_download_all -> viewModel.downloadAll()
R.id.menu_download -> startActivity<DownloadActivity>()
}
}

@ -4,7 +4,6 @@ import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookGroup
import io.legado.app.service.help.Download
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
@ -33,11 +32,4 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
}
}
fun downloadAll() {
execute {
App.db.bookDao().webBooks.forEach { book ->
Download.start(context, book.bookUrl, book.durChapterIndex, book.totalChapterNum)
}
}
}
}

@ -21,6 +21,7 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.AppConst
import io.legado.app.constant.Theme
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
@ -65,7 +66,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initData() {
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.findItem(R.id.menu_group_local)
.isChecked = AppConst.bookGroupLocalShow

@ -9,7 +9,6 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.ACache
import io.legado.app.utils.gone
@ -29,7 +28,6 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {
with(holder.itemView) {
if (payloads.isEmpty()) {
ATH.applyBackgroundTint(ll_title)
tv_name.text = item.bookSourceName
ll_title.onClick {
val oldEx = exIndex

@ -7,9 +7,11 @@ import android.view.MenuItem
import android.view.View
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseFragment
import io.legado.app.constant.Bus
import io.legado.app.help.BookHelp
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
@ -23,10 +25,7 @@ import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigViewModel
import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.utils.LogUtils
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.startActivity
import io.legado.app.utils.toast
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.startActivity
@ -64,7 +63,17 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
if (WebService.isRun) {
putPrefBoolean("webService", true)
} else {
putPrefBoolean("webService", false)
}
addPreferencesFromResource(R.xml.pref_main)
observeEvent<Boolean>(Bus.WEB_SERVICE_STOP) {
findPreference<SwitchPreference>("webService")?.let {
it.isChecked = false
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

@ -19,6 +19,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
@ -60,7 +61,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initData() {
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.group_manage)
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())

@ -11,6 +11,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import io.legado.app.R
import io.legado.app.constant.Theme
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
@ -53,7 +54,7 @@ class ReplaceEditDialog : DialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tool_bar.inflateMenu(R.menu.replace_edit)
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
viewModel.replaceRuleData.observe(this, Observer {
upReplaceView(it)

@ -126,6 +126,7 @@ class RssArticlesActivity : VMBaseActivity<RssArticlesViewModel>(R.layout.activi
override fun readRss(rssArticle: RssArticle) {
viewModel.read(rssArticle)
startActivity<ReadRssActivity>(
Pair("title", rssArticle.title),
Pair("origin", rssArticle.origin),
Pair("link", rssArticle.link)
)

@ -1,7 +1,9 @@
package io.legado.app.ui.rss.read
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.webkit.WebSettings
@ -14,6 +16,7 @@ import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_rss_read.*
import org.jetbrains.anko.toast
class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_read),
ReadRssViewModel.CallBack {
@ -25,7 +28,7 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
override fun onActivityCreated(savedInstanceState: Bundle?) {
viewModel.callBack = this
title = intent.getStringExtra("title")
title_bar.title = intent.getStringExtra("title")
initWebView()
initLiveData()
viewModel.initData(intent)
@ -41,6 +44,9 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_rss_star -> viewModel.star()
R.id.menu_share_it -> viewModel.rssArticle?.let {
shareText("链接分享", it.link)
}
}
return super.onCompatOptionsItemSelected(item)
}
@ -59,20 +65,11 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
viewModel.rssArticle?.let {
upJavaScriptEnable()
val url = NetworkUtils.getAbsoluteURL(it.origin, it.link)
val html = viewModel.clHtml(content)
if (viewModel.rssSource?.loadWithBaseUrl == true) {
webView.loadDataWithBaseURL(
url,
"<style>img{max-width:100%}</style>$content",
"text/html",
"utf-8",
url
)
webView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
} else {
webView.loadData(
"<style>img{max-width:100%}</style>$content",
"text/html",
"utf-8"
)
webView.loadData(html, "text/html", "utf-8")
}
}
})
@ -99,4 +96,39 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
}
DrawableUtils.setTint(starMenuItem?.icon, primaryTextColor)
}
override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> {
finish()
return true
}
}
return super.onKeyLongPress(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
event?.let {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> if (event.isTracking && !event.isCanceled && webView.canGoBack()) {
if (webView.copyBackForwardList().size > 1) {
webView.goBack()
return true
}
}
}
}
return super.onKeyUp(keyCode, event)
}
private fun shareText(title: String, text: String) {
try {
val textIntent = Intent(Intent.ACTION_SEND)
textIntent.type = "text/plain"
textIntent.putExtra(Intent.EXTRA_TEXT, text)
startActivity(Intent.createChooser(textIntent, title))
} catch (e: Exception) {
toast(R.string.can_not_share)
}
}
}

@ -77,6 +77,19 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
}
}
fun clHtml(content: String): String {
return """
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>img{max-width:100% !important; width:auto; height:auto;}</style>
</head>
<body style:"height:auto;max-width: 100%; width:auto;">
$content
</body></html>
"""
}
interface CallBack {
fun upStarMenu()
}

@ -19,6 +19,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
@ -60,7 +61,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initData() {
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.group_manage)
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())

@ -35,7 +35,6 @@ import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_rss_source.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.view_search.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult
@ -55,7 +54,6 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
private var groupMenu: SubMenu? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
initRecyclerView()
initSearchView()
initLiveDataGroup()

@ -2,21 +2,15 @@ package io.legado.app.ui.widget
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.Menu
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.material.appbar.AppBarLayout
import io.legado.app.R
import io.legado.app.lib.theme.DrawableUtils
import io.legado.app.lib.theme.primaryColor
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.activity
import io.legado.app.utils.getNavigationBarHeight
import io.legado.app.utils.getStatusBarHeight
@ -190,50 +184,6 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a
it.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUp)
}
}
val primaryTextColor = if (isInEditMode) Color.BLACK else context.primaryTextColor
DrawableUtils.setTint(toolbar.overflowIcon, primaryTextColor)
toolbar.setTitleTextColor(primaryTextColor)
if (navigationIconTint != null) {
wrapDrawableTint(toolbar.navigationIcon, navigationIconTint, navigationIconTintMode)
} else {
wrapDrawableTint(
toolbar.navigationIcon,
ColorStateList.valueOf(primaryTextColor),
navigationIconTintMode
)
}
}
private fun wrapDrawableTint(drawable: Drawable?, tintList: ColorStateList?, tintMode: Int) {
if (drawable == null || tintList == null) return
val wrappedDrawable = DrawableCompat.wrap(drawable.mutate())
DrawableCompat.setTintList(wrappedDrawable, tintList)
DrawableCompat.setTintMode(wrappedDrawable, intToMode(tintMode))
}
private fun intToMode(`val`: Int): PorterDuff.Mode {
when (`val`) {
0 -> return PorterDuff.Mode.CLEAR
1 -> return PorterDuff.Mode.SRC
2 -> return PorterDuff.Mode.DST
3 -> return PorterDuff.Mode.SRC_OVER
4 -> return PorterDuff.Mode.DST_OVER
5 -> return PorterDuff.Mode.SRC_IN
6 -> return PorterDuff.Mode.DST_IN
7 -> return PorterDuff.Mode.SRC_OUT
8 -> return PorterDuff.Mode.DST_OUT
9 -> return PorterDuff.Mode.SRC_ATOP
10 -> return PorterDuff.Mode.DST_ATOP
11 -> return PorterDuff.Mode.XOR
16 -> return PorterDuff.Mode.DARKEN
17 -> return PorterDuff.Mode.LIGHTEN
13 -> return PorterDuff.Mode.MULTIPLY
14 -> return PorterDuff.Mode.SCREEN
12 -> return PorterDuff.Mode.ADD
15 -> return PorterDuff.Mode.OVERLAY
else -> return PorterDuff.Mode.CLEAR
}
}
}

@ -1,954 +0,0 @@
package io.legado.app.ui.widget.page.curl;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.RectF;
import android.opengl.GLUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/**
* Class implementing actual curl/page rendering.
*
* @author harism
*/
public class CurlMesh {
// Flag for rendering some lines used for developing. Shows
// curl position and one for the direction from the
// position given. Comes handy once playing around with different
// ways for following pointer.
private static final boolean DRAW_CURL_POSITION = false;
// Flag for drawing polygon outlines. Using this flag crashes on emulator
// due to reason unknown to me. Leaving it here anyway as seeing polygon
// outlines gives good insight how original rectangle is divided.
private static final boolean DRAW_POLYGON_OUTLINES = false;
// Flag for enabling shadow rendering.
private static final boolean DRAW_SHADOW = true;
// Flag for texture rendering. While this is likely something you
// don't want to do it's been used for development purposes as texture
// rendering is rather slow on emulator.
private static final boolean DRAW_TEXTURE = true;
// Colors for shadow. Inner one is the color drawn next to surface where
// shadowed area starts and outer one is color shadow ends to.
private static final float[] SHADOW_INNER_COLOR = {0f, 0f, 0f, .5f};
private static final float[] SHADOW_OUTER_COLOR = {0f, 0f, 0f, .0f};
// Let's avoid using 'new' as much as possible. Meaning we introduce arrays
// once here and reuse them on runtime. Doesn't really have very much effect
// but avoids some garbage collections from happening.
private Array<ShadowVertex> mArrDropShadowVertices;
private Array<Vertex> mArrIntersections;
private Array<Vertex> mArrOutputVertices;
private Array<Vertex> mArrRotatedVertices;
private Array<Double> mArrScanLines;
private Array<ShadowVertex> mArrSelfShadowVertices;
private Array<ShadowVertex> mArrTempShadowVertices;
private Array<Vertex> mArrTempVertices;
// Buffers for feeding rasterizer.
private FloatBuffer mBufColors;
private FloatBuffer mBufCurlPositionLines;
private FloatBuffer mBufShadowColors;
private FloatBuffer mBufShadowVertices;
private FloatBuffer mBufTexCoords;
private FloatBuffer mBufVertices;
private int mCurlPositionLinesCount;
private int mDropShadowCount;
// Boolean for 'flipping' texture sideways.
private boolean mFlipTexture = false;
// Maximum number of split lines used for creating a curl.
private int mMaxCurlSplits;
// Bounding rectangle for this mesh. mRectagle[0] = top-left corner,
// mRectangle[1] = bottom-left, mRectangle[2] = top-right and mRectangle[3]
// bottom-right.
private final Vertex[] mRectangle = new Vertex[4];
private int mSelfShadowCount;
private boolean mTextureBack = false;
// Texture ids and other variables.
private int[] mTextureIds = null;
private final CurlPage mTexturePage = new CurlPage();
private final RectF mTextureRectBack = new RectF();
private final RectF mTextureRectFront = new RectF();
private int mVerticesCountBack;
private int mVerticesCountFront;
/**
* Constructor for mesh object.
*
* @param maxCurlSplits Maximum number curl can be divided into. The bigger the value
* the smoother curl will be. With the cost of having more
* polygons for drawing.
*/
public CurlMesh(int maxCurlSplits) {
// There really is no use for 0 splits.
mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits;
mArrScanLines = new Array<>(maxCurlSplits + 2);
mArrOutputVertices = new Array<>(7);
mArrRotatedVertices = new Array<>(4);
mArrIntersections = new Array<>(2);
mArrTempVertices = new Array<>(7 + 4);
for (int i = 0; i < 7 + 4; ++i) {
mArrTempVertices.add(new Vertex());
}
if (DRAW_SHADOW) {
mArrSelfShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
mArrDropShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
mArrTempShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) {
mArrTempShadowVertices.add(new ShadowVertex());
}
}
// Rectangle consists of 4 vertices. Index 0 = top-left, index 1 =
// bottom-left, index 2 = top-right and index 3 = bottom-right.
for (int i = 0; i < 4; ++i) {
mRectangle[i] = new Vertex();
}
// Set up shadow penumbra direction to each vertex. We do fake 'self
// shadow' calculations based on this information.
mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1;
mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1;
if (DRAW_CURL_POSITION) {
mCurlPositionLinesCount = 3;
ByteBuffer hvbb = ByteBuffer
.allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4);
hvbb.order(ByteOrder.nativeOrder());
mBufCurlPositionLines = hvbb.asFloatBuffer();
mBufCurlPositionLines.position(0);
}
// There are 4 vertices from bounding rect, max 2 from adding split line
// to two corners and curl consists of max mMaxCurlSplits lines each
// outputting 2 vertices.
int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits);
ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4);
vbb.order(ByteOrder.nativeOrder());
mBufVertices = vbb.asFloatBuffer();
mBufVertices.position(0);
if (DRAW_TEXTURE) {
ByteBuffer tbb = ByteBuffer
.allocateDirect(maxVerticesCount * 2 * 4);
tbb.order(ByteOrder.nativeOrder());
mBufTexCoords = tbb.asFloatBuffer();
mBufTexCoords.position(0);
}
ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4);
cbb.order(ByteOrder.nativeOrder());
mBufColors = cbb.asFloatBuffer();
mBufColors.position(0);
if (DRAW_SHADOW) {
int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2;
ByteBuffer scbb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 4 * 4);
scbb.order(ByteOrder.nativeOrder());
mBufShadowColors = scbb.asFloatBuffer();
mBufShadowColors.position(0);
ByteBuffer sibb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 3 * 4);
sibb.order(ByteOrder.nativeOrder());
mBufShadowVertices = sibb.asFloatBuffer();
mBufShadowVertices.position(0);
mDropShadowCount = mSelfShadowCount = 0;
}
}
/**
* Adds vertex to buffers.
*/
private void addVertex(Vertex vertex) {
mBufVertices.put((float) vertex.mPosX);
mBufVertices.put((float) vertex.mPosY);
mBufVertices.put((float) vertex.mPosZ);
mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f);
mBufColors.put(Color.alpha(vertex.mColor) / 255f);
if (DRAW_TEXTURE) {
mBufTexCoords.put((float) vertex.mTexX);
mBufTexCoords.put((float) vertex.mTexY);
}
}
/**
* Sets curl for this mesh.
*
* @param curlPos Position for curl 'center'. Can be any point on line collinear
* to curl.
* @param curlDir Curl direction, should be normalized.
* @param radius Radius of curl.
*/
public synchronized void curl(PointF curlPos, PointF curlDir, double radius) {
// First add some 'helper' lines used for development.
if (DRAW_CURL_POSITION) {
mBufCurlPositionLines.position(0);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y - 1.0f);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y + 1.0f);
mBufCurlPositionLines.put(curlPos.x - 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2);
mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2);
mBufCurlPositionLines.position(0);
}
// Actual 'curl' implementation starts here.
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
// Calculate curl angle from direction.
double curlAngle = Math.acos(curlDir.x);
curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle;
// Initiate rotated rectangle which's is translated to curlPos and
// rotated so that curl direction heads to right (1,0). Vertices are
// ordered in ascending order based on x -coordinate at the same time.
// And using y -coordinate in very rare case in which two vertices have
// same x -coordinate.
mArrTempVertices.addAll(mArrRotatedVertices);
mArrRotatedVertices.clear();
for (int i = 0; i < 4; ++i) {
Vertex v = mArrTempVertices.remove(0);
v.set(mRectangle[i]);
v.translate(-curlPos.x, -curlPos.y);
v.rotateZ(-curlAngle);
int j = 0;
for (; j < mArrRotatedVertices.size(); ++j) {
Vertex v2 = mArrRotatedVertices.get(j);
if (v.mPosX > v2.mPosX) {
break;
}
if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) {
break;
}
}
mArrRotatedVertices.add(j, v);
}
// Rotated rectangle lines/vertex indices. We need to find bounding
// lines for rotated rectangle. After sorting vertices according to
// their x -coordinate we don't have to worry about vertices at indices
// 0 and 1. But due to inaccuracy it's possible vertex 3 is not the
// opposing corner from vertex 0. So we are calculating distance from
// vertex 0 to vertices 2 and 3 - and altering line indices if needed.
// Also vertices/lines are given in an order first one has x -coordinate
// at least the latter one. This property is used in getIntersections to
// see if there is an intersection.
int[][] lines = {{0, 1}, {0, 2}, {1, 3}, {2, 3}};
{
// TODO: There really has to be more 'easier' way of doing this -
// not including extensive use of sqrt.
Vertex v0 = mArrRotatedVertices.get(0);
Vertex v2 = mArrRotatedVertices.get(2);
Vertex v3 = mArrRotatedVertices.get(3);
double dist2 = Math.sqrt((v0.mPosX - v2.mPosX)
* (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY)
* (v0.mPosY - v2.mPosY));
double dist3 = Math.sqrt((v0.mPosX - v3.mPosX)
* (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY)
* (v0.mPosY - v3.mPosY));
if (dist2 > dist3) {
lines[1][1] = 3;
lines[2][1] = 2;
}
}
mVerticesCountFront = mVerticesCountBack = 0;
if (DRAW_SHADOW) {
mArrTempShadowVertices.addAll(mArrDropShadowVertices);
mArrTempShadowVertices.addAll(mArrSelfShadowVertices);
mArrDropShadowVertices.clear();
mArrSelfShadowVertices.clear();
}
// Length of 'curl' curve.
double curlLength = Math.PI * radius;
// Calculate scan lines.
// TODO: Revisit this code one day. There is room for optimization here.
mArrScanLines.clear();
if (mMaxCurlSplits > 0) {
mArrScanLines.add((double) 0);
}
for (int i = 1; i < mMaxCurlSplits; ++i) {
mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1));
}
// As mRotatedVertices is ordered regarding x -coordinate, adding
// this scan line produces scan area picking up vertices which are
// rotated completely. One could say 'until infinity'.
mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1);
// Start from right most vertex. Pretty much the same as first scan area
// is starting from 'infinity'.
double scanXmax = mArrRotatedVertices.get(0).mPosX + 1;
for (int i = 0; i < mArrScanLines.size(); ++i) {
// Once we have scanXmin and scanXmax we have a scan area to start
// working with.
double scanXmin = mArrScanLines.get(i);
// First iterate 'original' rectangle vertices within scan area.
for (int j = 0; j < mArrRotatedVertices.size(); ++j) {
Vertex v = mArrRotatedVertices.get(j);
// Test if vertex lies within this scan area.
// TODO: Frankly speaking, can't remember why equality check was
// added to both ends. Guessing it was somehow related to case
// where radius=0f, which, given current implementation, could
// be handled much more effectively anyway.
if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) {
// Pop out a vertex from temp vertices.
Vertex n = mArrTempVertices.remove(0);
n.set(v);
// This is done solely for triangulation reasons. Given a
// rotated rectangle it has max 2 vertices having
// intersection.
Array<Vertex> intersections = getIntersections(
mArrRotatedVertices, lines, n.mPosX);
// In a sense one could say we're adding vertices always in
// two, positioned at the ends of intersecting line. And for
// triangulation to work properly they are added based on y
// -coordinate. And this if-else is doing it for us.
if (intersections.size() == 1
&& intersections.get(0).mPosY > v.mPosY) {
// In case intersecting vertex is higher add it first.
mArrOutputVertices.addAll(intersections);
mArrOutputVertices.add(n);
} else if (intersections.size() <= 1) {
// Otherwise add original vertex first.
mArrOutputVertices.add(n);
mArrOutputVertices.addAll(intersections);
} else {
// There should never be more than 1 intersecting
// vertex. But if it happens as a fallback simply skip
// everything.
mArrTempVertices.add(n);
mArrTempVertices.addAll(intersections);
}
}
}
// Search for scan line intersections.
Array<Vertex> intersections = getIntersections(mArrRotatedVertices,
lines, scanXmin);
// We expect to get 0 or 2 vertices. In rare cases there's only one
// but in general given a scan line intersecting rectangle there
// should be 2 intersecting vertices.
if (intersections.size() == 2) {
// There were two intersections, add them based on y
// -coordinate, higher first, lower last.
Vertex v1 = intersections.get(0);
Vertex v2 = intersections.get(1);
if (v1.mPosY < v2.mPosY) {
mArrOutputVertices.add(v2);
mArrOutputVertices.add(v1);
} else {
mArrOutputVertices.addAll(intersections);
}
} else if (intersections.size() != 0) {
// This happens in a case in which there is a original vertex
// exactly at scan line or something went very much wrong if
// there are 3+ vertices. What ever the reason just return the
// vertices to temp vertices for later use. In former case it
// was handled already earlier once iterating through
// mRotatedVertices, in latter case it's better to avoid doing
// anything with them.
mArrTempVertices.addAll(intersections);
}
// Add vertices found during this iteration to vertex etc buffers.
while (mArrOutputVertices.size() > 0) {
Vertex v = mArrOutputVertices.remove(0);
mArrTempVertices.add(v);
// Local texture front-facing flag.
boolean textureFront;
// Untouched vertices.
if (i == 0) {
textureFront = true;
mVerticesCountFront++;
}
// 'Completely' rotated vertices.
else if (i == mArrScanLines.size() - 1 || curlLength == 0) {
v.mPosX = -(curlLength + v.mPosX);
v.mPosZ = 2 * radius;
v.mPenumbraX = -v.mPenumbraX;
textureFront = false;
mVerticesCountBack++;
}
// Vertex lies within 'curl'.
else {
// Even though it's not obvious from the if-else clause,
// here v.mPosX is between [-curlLength, 0]. And we can do
// calculations around a half cylinder.
double rotY = Math.PI * (v.mPosX / curlLength);
v.mPosX = radius * Math.sin(rotY);
v.mPosZ = radius - (radius * Math.cos(rotY));
v.mPenumbraX *= Math.cos(rotY);
// Map color multiplier to [.1f, 1f] range.
v.mColorFactor = (float) (.1f + .9f * Math.sqrt(Math
.sin(rotY) + 1));
if (v.mPosZ >= radius) {
textureFront = false;
mVerticesCountBack++;
} else {
textureFront = true;
mVerticesCountFront++;
}
}
// We use local textureFront for flipping backside texture
// locally. Plus additionally if mesh is in flip texture mode,
// we'll make the procedure "backwards". Also, until this point,
// texture coordinates are within [0, 1] range so we'll adjust
// them to final texture coordinates too.
if (textureFront != mFlipTexture) {
v.mTexX *= mTextureRectFront.right;
v.mTexY *= mTextureRectFront.bottom;
v.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
} else {
v.mTexX *= mTextureRectBack.right;
v.mTexY *= mTextureRectBack.bottom;
v.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
}
// Move vertex back to 'world' coordinates.
v.rotateZ(curlAngle);
v.translate(curlPos.x, curlPos.y);
addVertex(v);
// Drop shadow is cast 'behind' the curl.
if (DRAW_SHADOW && v.mPosZ > 0 && v.mPosZ <= radius) {
ShadowVertex sv = mArrTempShadowVertices.remove(0);
sv.mPosX = v.mPosX;
sv.mPosY = v.mPosY;
sv.mPosZ = v.mPosZ;
sv.mPenumbraX = (v.mPosZ / 2) * -curlDir.x;
sv.mPenumbraY = (v.mPosZ / 2) * -curlDir.y;
sv.mPenumbraColor = v.mPosZ / radius;
int idx = (mArrDropShadowVertices.size() + 1) / 2;
mArrDropShadowVertices.add(idx, sv);
}
// Self shadow is cast partly over mesh.
if (DRAW_SHADOW && v.mPosZ > radius) {
ShadowVertex sv = mArrTempShadowVertices.remove(0);
sv.mPosX = v.mPosX;
sv.mPosY = v.mPosY;
sv.mPosZ = v.mPosZ;
sv.mPenumbraX = ((v.mPosZ - radius) / 3) * v.mPenumbraX;
sv.mPenumbraY = ((v.mPosZ - radius) / 3) * v.mPenumbraY;
sv.mPenumbraColor = (v.mPosZ - radius) / (2 * radius);
int idx = (mArrSelfShadowVertices.size() + 1) / 2;
mArrSelfShadowVertices.add(idx, sv);
}
}
// Switch scanXmin as scanXmax for next iteration.
scanXmax = scanXmin;
}
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
// Add shadow Vertices.
if (DRAW_SHADOW) {
mBufShadowColors.position(0);
mBufShadowVertices.position(0);
mDropShadowCount = 0;
for (int i = 0; i < mArrDropShadowVertices.size(); ++i) {
ShadowVertex sv = mArrDropShadowVertices.get(i);
mBufShadowVertices.put((float) sv.mPosX);
mBufShadowVertices.put((float) sv.mPosY);
mBufShadowVertices.put((float) sv.mPosZ);
mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
mBufShadowVertices.put((float) sv.mPosZ);
for (int j = 0; j < 4; ++j) {
double color = SHADOW_OUTER_COLOR[j]
+ (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
* sv.mPenumbraColor;
mBufShadowColors.put((float) color);
}
mBufShadowColors.put(SHADOW_OUTER_COLOR);
mDropShadowCount += 2;
}
mSelfShadowCount = 0;
for (int i = 0; i < mArrSelfShadowVertices.size(); ++i) {
ShadowVertex sv = mArrSelfShadowVertices.get(i);
mBufShadowVertices.put((float) sv.mPosX);
mBufShadowVertices.put((float) sv.mPosY);
mBufShadowVertices.put((float) sv.mPosZ);
mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
mBufShadowVertices.put((float) sv.mPosZ);
for (int j = 0; j < 4; ++j) {
double color = SHADOW_OUTER_COLOR[j]
+ (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
* sv.mPenumbraColor;
mBufShadowColors.put((float) color);
}
mBufShadowColors.put(SHADOW_OUTER_COLOR);
mSelfShadowCount += 2;
}
mBufShadowColors.position(0);
mBufShadowVertices.position(0);
}
}
/**
* Calculates intersections for given scan line.
*/
private Array<Vertex> getIntersections(Array<Vertex> vertices,
int[][] lineIndices, double scanX) {
mArrIntersections.clear();
// Iterate through rectangle lines each re-presented as a pair of
// vertices.
for (int[] lineIndex : lineIndices) {
Vertex v1 = vertices.get(lineIndex[0]);
Vertex v2 = vertices.get(lineIndex[1]);
// Here we expect that v1.mPosX >= v2.mPosX and wont do intersection
// test the opposite way.
if (v1.mPosX > scanX && v2.mPosX < scanX) {
// There is an intersection, calculate coefficient telling 'how
// far' scanX is from v2.
double c = (scanX - v2.mPosX) / (v1.mPosX - v2.mPosX);
Vertex n = mArrTempVertices.remove(0);
n.set(v2);
n.mPosX = scanX;
n.mPosY += (v1.mPosY - v2.mPosY) * c;
if (DRAW_TEXTURE) {
n.mTexX += (v1.mTexX - v2.mTexX) * c;
n.mTexY += (v1.mTexY - v2.mTexY) * c;
}
if (DRAW_SHADOW) {
n.mPenumbraX += (v1.mPenumbraX - v2.mPenumbraX) * c;
n.mPenumbraY += (v1.mPenumbraY - v2.mPenumbraY) * c;
}
mArrIntersections.add(n);
}
}
return mArrIntersections;
}
/**
* Getter for textures page for this mesh.
*/
public synchronized CurlPage getTexturePage() {
return mTexturePage;
}
/**
* Renders our page curl mesh.
*/
public synchronized void onDrawFrame(GL10 gl) {
// First allocate texture if there is not one yet.
if (DRAW_TEXTURE && mTextureIds == null) {
// Generate texture.
mTextureIds = new int[2];
gl.glGenTextures(2, mTextureIds, 0);
for (int textureId : mTextureIds) {
// Set texture attributes.
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);
}
}
if (DRAW_TEXTURE && mTexturePage.getTexturesChanged()) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
Bitmap texture = mTexturePage.getTexture(mTextureRectFront,
CurlPage.SIDE_FRONT);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
texture.recycle();
mTextureBack = mTexturePage.hasBackTexture();
if (mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
texture = mTexturePage.getTexture(mTextureRectBack,
CurlPage.SIDE_BACK);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
texture.recycle();
} else {
mTextureRectBack.set(mTextureRectFront);
}
mTexturePage.recycle();
reset();
}
// Some 'global' settings.
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// TODO: Drop shadow drawing is done temporarily here to hide some
// problems with its calculation.
if (DRAW_SHADOW) {
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mDropShadowCount);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_TEXTURE) {
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufTexCoords);
}
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
// Enable color array.
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufColors);
// Draw front facing blank vertices.
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
// Draw front facing texture.
if (DRAW_TEXTURE) {
gl.glEnable(GL10.GL_BLEND);
gl.glEnable(GL10.GL_TEXTURE_2D);
if (!mFlipTexture || !mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
} else {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
}
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
gl.glDisable(GL10.GL_BLEND);
gl.glDisable(GL10.GL_TEXTURE_2D);
}
int backStartIdx = Math.max(0, mVerticesCountFront - 2);
int backCount = mVerticesCountFront + mVerticesCountBack - backStartIdx;
// Draw back facing blank vertices.
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
// Draw back facing texture.
if (DRAW_TEXTURE) {
gl.glEnable(GL10.GL_BLEND);
gl.glEnable(GL10.GL_TEXTURE_2D);
if (mFlipTexture || !mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
} else {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
}
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
gl.glDisable(GL10.GL_BLEND);
gl.glDisable(GL10.GL_TEXTURE_2D);
}
// Disable textures and color array.
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
if (DRAW_POLYGON_OUTLINES) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glLineWidth(1.0f);
gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mVerticesCountFront);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_CURL_POSITION) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glLineWidth(1.0f);
gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f);
gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mBufCurlPositionLines);
gl.glDrawArrays(GL10.GL_LINES, 0, mCurlPositionLinesCount * 2);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_SHADOW) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, mDropShadowCount,
mSelfShadowCount);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
gl.glDisable(GL10.GL_BLEND);
}
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}
/**
* Resets mesh to 'initial' state. Meaning this mesh will draw a plain
* textured rectangle after call to this method.
*/
public synchronized void reset() {
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
for (int i = 0; i < 4; ++i) {
Vertex tmp = mArrTempVertices.get(0);
tmp.set(mRectangle[i]);
if (mFlipTexture) {
tmp.mTexX *= mTextureRectBack.right;
tmp.mTexY *= mTextureRectBack.bottom;
tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
} else {
tmp.mTexX *= mTextureRectFront.right;
tmp.mTexY *= mTextureRectFront.bottom;
tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
}
addVertex(tmp);
}
mVerticesCountFront = 4;
mVerticesCountBack = 0;
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
mDropShadowCount = mSelfShadowCount = 0;
}
/**
* Resets allocated texture id forcing creation of new one. After calling
* this method you most likely want to set bitmap too as it's lost. This
* method should be called only once e.g GL context is re-created as this
* method does not release previous texture id, only makes sure new one is
* requested on next render.
*/
public synchronized void resetTexture() {
mTextureIds = null;
}
/**
* If true, flips texture sideways.
*/
public synchronized void setFlipTexture(boolean flipTexture) {
mFlipTexture = flipTexture;
if (flipTexture) {
setTexCoords(1f, 0f, 0f, 1f);
} else {
setTexCoords(0f, 0f, 1f, 1f);
}
}
/**
* Update mesh bounds.
*/
public void setRect(RectF r) {
mRectangle[0].mPosX = r.left;
mRectangle[0].mPosY = r.top;
mRectangle[1].mPosX = r.left;
mRectangle[1].mPosY = r.bottom;
mRectangle[2].mPosX = r.right;
mRectangle[2].mPosY = r.top;
mRectangle[3].mPosX = r.right;
mRectangle[3].mPosY = r.bottom;
}
/**
* Sets texture coordinates to mRectangle vertices.
*/
private synchronized void setTexCoords(float left, float top, float right,
float bottom) {
mRectangle[0].mTexX = left;
mRectangle[0].mTexY = top;
mRectangle[1].mTexX = left;
mRectangle[1].mTexY = bottom;
mRectangle[2].mTexX = right;
mRectangle[2].mTexY = top;
mRectangle[3].mTexX = right;
mRectangle[3].mTexY = bottom;
}
/**
* Simple fixed size array implementation.
*/
private class Array<T> {
private Object[] mArray;
private int mCapacity;
private int mSize;
Array(int capacity) {
mCapacity = capacity;
mArray = new Object[capacity];
}
public void add(int index, T item) {
if (index < 0 || index > mSize || mSize >= mCapacity) {
throw new IndexOutOfBoundsException();
}
System.arraycopy(mArray, index, mArray, index + 1, mSize - index);
mArray[index] = item;
++mSize;
}
public void add(T item) {
if (mSize >= mCapacity) {
throw new IndexOutOfBoundsException();
}
mArray[mSize++] = item;
}
public void addAll(Array<T> array) {
if (mSize + array.size() > mCapacity) {
throw new IndexOutOfBoundsException();
}
for (int i = 0; i < array.size(); ++i) {
mArray[mSize++] = array.get(i);
}
}
public void clear() {
mSize = 0;
}
@SuppressWarnings("unchecked")
public T get(int index) {
if (index < 0 || index >= mSize) {
throw new IndexOutOfBoundsException();
}
return (T) mArray[index];
}
@SuppressWarnings("unchecked")
public T remove(int index) {
if (index < 0 || index >= mSize) {
throw new IndexOutOfBoundsException();
}
T item = (T) mArray[index];
if (mSize - 1 - index >= 0)
System.arraycopy(mArray, index + 1, mArray, index, mSize - 1 - index);
--mSize;
return item;
}
public int size() {
return mSize;
}
}
/**
* Holder for shadow vertex information.
*/
private class ShadowVertex {
double mPenumbraColor;
double mPenumbraX;
double mPenumbraY;
double mPosX;
double mPosY;
double mPosZ;
}
/**
* Holder for vertex information.
*/
private class Vertex {
int mColor;
float mColorFactor;
double mPenumbraX;
double mPenumbraY;
double mPosX;
double mPosY;
double mPosZ;
double mTexX;
double mTexY;
Vertex() {
mPosX = mPosY = mPosZ = mTexX = mTexY = 0;
mColorFactor = 1.0f;
}
void rotateZ(double theta) {
double cos = Math.cos(theta);
double sin = Math.sin(theta);
double x = mPosX * cos + mPosY * sin;
double y = mPosX * -sin + mPosY * cos;
mPosX = x;
mPosY = y;
double px = mPenumbraX * cos + mPenumbraY * sin;
double py = mPenumbraX * -sin + mPenumbraY * cos;
mPenumbraX = px;
mPenumbraY = py;
}
public void set(Vertex vertex) {
mPosX = vertex.mPosX;
mPosY = vertex.mPosY;
mPosZ = vertex.mPosZ;
mTexX = vertex.mTexX;
mTexY = vertex.mTexY;
mPenumbraX = vertex.mPenumbraX;
mPenumbraY = vertex.mPenumbraY;
mColor = vertex.mColor;
mColorFactor = vertex.mColorFactor;
}
public void translate(double dx, double dy) {
mPosX += dx;
mPosY += dy;
}
}
}

@ -1,191 +0,0 @@
package io.legado.app.ui.widget.page.curl
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.RectF
/**
* Storage class for page textures, blend colors and possibly some other values
* in the future.
*
* @author harism
*/
class CurlPage {
private var mColorBack: Int = 0
private var mColorFront: Int = 0
private var mTextureBack: Bitmap? = null
private var mTextureFront: Bitmap? = null
/**
* Returns true if textures have changed.
*/
var texturesChanged: Boolean = false
private set
/**
* Default constructor.
*/
init {
reset()
}
/**
* Getter for color.
*/
fun getColor(side: Int): Int {
return when (side) {
SIDE_FRONT -> mColorFront
else -> mColorBack
}
}
/**
* Calculates the next highest power of two for a given integer.
*/
private fun getNextHighestPO2(n: Int): Int {
var n1 = n
n1 -= 1
n1 = n1 or (n1 shr 1)
n1 = n1 or (n1 shr 2)
n1 = n1 or (n1 shr 4)
n1 = n1 or (n1 shr 8)
n1 = n1 or (n1 shr 16)
n1 = n1 or (n1 shr 32)
return n1 + 1
}
/**
* Generates nearest power of two sized Bitmap for give Bitmap. Returns this
* new Bitmap using default return statement + original texture coordinates
* are stored into RectF.
*/
private fun getTexture(bitmap: Bitmap, textureRect: RectF): Bitmap {
// Bitmap original size.
val w = bitmap.width
val h = bitmap.height
// Bitmap size expanded to next power of two. This is done due to
// the requirement on many devices, texture width and height should
// be power of two.
val newW = getNextHighestPO2(w)
val newH = getNextHighestPO2(h)
// TODO: Is there another way to create a bigger Bitmap and copy
// original Bitmap to it more efficiently? Immutable bitmap anyone?
val bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.config)
val c = Canvas(bitmapTex)
c.drawBitmap(bitmap, 0f, 0f, null)
// Calculate final texture coordinates.
val texX = w.toFloat() / newW
val texY = h.toFloat() / newH
textureRect.set(0f, 0f, texX, texY)
return bitmapTex
}
/**
* Getter for textures. Creates Bitmap sized to nearest power of two, copies
* original Bitmap into it and returns it. RectF given as parameter is
* filled with actual texture coordinates in this new upscaled texture
* Bitmap.
*/
fun getTexture(textureRect: RectF, side: Int): Bitmap {
return when (side) {
SIDE_FRONT -> getTexture(mTextureFront!!, textureRect)
else -> getTexture(mTextureBack!!, textureRect)
}
}
/**
* Returns true if back siding texture exists and it differs from front
* facing one.
*/
fun hasBackTexture(): Boolean {
return mTextureFront != mTextureBack
}
/**
* Recycles and frees underlying Bitmaps.
*/
fun recycle() {
if (mTextureFront != null) {
mTextureFront!!.recycle()
}
mTextureFront = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
mTextureFront!!.eraseColor(mColorFront)
if (mTextureBack != null) {
mTextureBack!!.recycle()
}
mTextureBack = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
mTextureBack!!.eraseColor(mColorBack)
texturesChanged = false
}
/**
* Resets this CurlPage into its initial state.
*/
fun reset() {
mColorBack = Color.WHITE
mColorFront = Color.WHITE
recycle()
}
/**
* Setter blend color.
*/
fun setColor(color: Int, side: Int) {
when (side) {
SIDE_FRONT -> mColorFront = color
SIDE_BACK -> mColorBack = color
else -> {
mColorBack = color
mColorFront = mColorBack
}
}
}
/**
* Setter for textures.
*/
fun setTexture(texture: Bitmap?, side: Int) {
var texture1 = texture
if (texture1 == null) {
texture1 = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
if (side == SIDE_BACK) {
texture1!!.eraseColor(mColorBack)
} else {
texture1!!.eraseColor(mColorFront)
}
}
when (side) {
SIDE_FRONT -> {
if (mTextureFront != null)
mTextureFront!!.recycle()
mTextureFront = texture1
}
SIDE_BACK -> {
if (mTextureBack != null)
mTextureBack!!.recycle()
mTextureBack = texture1
}
SIDE_BOTH -> {
if (mTextureFront != null)
mTextureFront!!.recycle()
if (mTextureBack != null)
mTextureBack!!.recycle()
mTextureBack = texture1
mTextureFront = mTextureBack
}
}
texturesChanged = true
}
companion object {
const val SIDE_BACK = 2
const val SIDE_BOTH = 3
const val SIDE_FRONT = 1
}
}

@ -1,221 +0,0 @@
package io.legado.app.ui.widget.page.curl
import android.graphics.Color
import android.graphics.PointF
import android.graphics.RectF
import android.opengl.GLSurfaceView
import android.opengl.GLU
import java.util.*
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
/**
* Actual renderer class.
*
* @author harism
*/
class CurlRenderer(private val mObserver: Observer) : GLSurfaceView.Renderer {
// Background fill color.
private var mBackgroundColor: Int = 0
// Curl meshes used for static and dynamic rendering.
private val mCurlMeshes: Vector<CurlMesh> = Vector()
private val mMargins = RectF()
// Page rectangles.
private val mPageRectLeft: RectF = RectF()
private val mPageRectRight: RectF = RectF()
// View mode.
private var mViewMode = SHOW_ONE_PAGE
// Screen size.
private var mViewportWidth: Int = 0
private var mViewportHeight: Int = 0
// Rect for render area.
private val mViewRect = RectF()
/**
* Adds CurlMesh to this renderer.
*/
@Synchronized
fun addCurlMesh(mesh: CurlMesh) {
removeCurlMesh(mesh)
mCurlMeshes.add(mesh)
}
/**
* Returns rect reserved for left or right page. Value page should be
* PAGE_LEFT or PAGE_RIGHT.
*/
fun getPageRect(page: Int): RectF? {
if (page == PAGE_LEFT) {
return mPageRectLeft
} else if (page == PAGE_RIGHT) {
return mPageRectRight
}
return null
}
@Synchronized
override fun onDrawFrame(gl: GL10) {
mObserver.onDrawFrame()
gl.glClearColor(
Color.red(mBackgroundColor) / 255f,
Color.green(mBackgroundColor) / 255f,
Color.blue(mBackgroundColor) / 255f,
Color.alpha(mBackgroundColor) / 255f
)
gl.glClear(GL10.GL_COLOR_BUFFER_BIT)
gl.glLoadIdentity()
if (!mObserver.canDraw) {
return
}
for (i in mCurlMeshes.indices) {
mCurlMeshes[i].onDrawFrame(gl)
}
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
gl.glViewport(0, 0, width, height)
mViewportWidth = width
mViewportHeight = height
val ratio = width.toFloat() / height
mViewRect.top = 1.0f
mViewRect.bottom = -1.0f
mViewRect.left = -ratio
mViewRect.right = ratio
updatePageRect()
gl.glMatrixMode(GL10.GL_PROJECTION)
gl.glLoadIdentity()
GLU.gluOrtho2D(
gl, mViewRect.left, mViewRect.right,
mViewRect.bottom, mViewRect.top
)
gl.glMatrixMode(GL10.GL_MODELVIEW)
gl.glLoadIdentity()
}
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
gl.glClearColor(0f, 0f, 0f, 1f)
gl.glShadeModel(GL10.GL_SMOOTH)
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST)
gl.glHint(GL10.GL_LINE_SMOOTH_HINT, GL10.GL_NICEST)
gl.glHint(GL10.GL_POLYGON_SMOOTH_HINT, GL10.GL_NICEST)
gl.glEnable(GL10.GL_LINE_SMOOTH)
gl.glDisable(GL10.GL_DEPTH_TEST)
gl.glDisable(GL10.GL_CULL_FACE)
mObserver.onSurfaceCreated()
}
/**
* Removes CurlMesh from this renderer.
*/
@Synchronized
fun removeCurlMesh(mesh: CurlMesh) {
mCurlMeshes.remove(mesh)
}
/**
* Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or
* SHOW_TWO_PAGES.
*/
@Synchronized
fun setViewMode(viewMode: Int) {
if (viewMode == SHOW_ONE_PAGE) {
mViewMode = viewMode
updatePageRect()
} else if (viewMode == SHOW_TWO_PAGES) {
mViewMode = viewMode
updatePageRect()
}
}
/**
* Translates screen coordinates into view coordinates.
*/
fun translate(pt: PointF) {
pt.x = mViewRect.left + mViewRect.width() * pt.x / mViewportWidth
pt.y = mViewRect.top - -mViewRect.height() * pt.y / mViewportHeight
}
/**
* Recalculates page rectangles.
*/
private fun updatePageRect() {
if (mViewRect.width() == 0f || mViewRect.height() == 0f) {
return
} else if (mViewMode == SHOW_ONE_PAGE) {
mPageRectRight.set(mViewRect)
mPageRectRight.left += mViewRect.width() * mMargins.left
mPageRectRight.right -= mViewRect.width() * mMargins.right
mPageRectRight.top += mViewRect.height() * mMargins.top
mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom
mPageRectLeft.set(mPageRectRight)
mPageRectLeft.offset(-mPageRectRight.width(), 0f)
val bitmapW = (mPageRectRight.width() * mViewportWidth / mViewRect
.width()).toInt()
val bitmapH = (mPageRectRight.height() * mViewportHeight / mViewRect
.height()).toInt()
mObserver.onPageSizeChanged(bitmapW, bitmapH)
} else if (mViewMode == SHOW_TWO_PAGES) {
mPageRectRight.set(mViewRect)
mPageRectRight.left += mViewRect.width() * mMargins.left
mPageRectRight.right -= mViewRect.width() * mMargins.right
mPageRectRight.top += mViewRect.height() * mMargins.top
mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom
mPageRectLeft.set(mPageRectRight)
mPageRectLeft.right = (mPageRectLeft.right + mPageRectLeft.left) / 2
mPageRectRight.left = mPageRectLeft.right
val bitmapW = (mPageRectRight.width() * mViewportWidth / mViewRect
.width()).toInt()
val bitmapH = (mPageRectRight.height() * mViewportHeight / mViewRect
.height()).toInt()
mObserver.onPageSizeChanged(bitmapW, bitmapH)
}
}
/**
* Observer for waiting render engine/state updates.
*/
interface Observer {
/**
* Called from onDrawFrame called before rendering is started. This is
* intended to be used for animation purposes.
*/
fun onDrawFrame()
/**
* Called once page size is changed. Width and height tell the page size
* in pixels making it possible to update textures accordingly.
*/
fun onPageSizeChanged(width: Int, height: Int)
/**
* Called from onSurfaceCreated to enable texture re-initialization etc
* what needs to be done when this happens.
*/
fun onSurfaceCreated()
var canDraw: Boolean
}
companion object {
// Constant for requesting left page rect.
const val PAGE_LEFT = 1
// Constant for requesting right page rect.
const val PAGE_RIGHT = 2
// Constants for changing view mode.
const val SHOW_ONE_PAGE = 1
const val SHOW_TWO_PAGES = 2
}
}

@ -1,769 +0,0 @@
package io.legado.app.ui.widget.page.curl
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.PointF
import android.opengl.GLSurfaceView
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
import kotlin.math.sqrt
/**
* OpenGL ES View.
*
* @author harism
*/
class CurlView : GLSurfaceView, View.OnTouchListener, CurlRenderer.Observer {
var callBack: CallBack? = null
private var mAllowLastPageCurl = true
private var mAnimate = false
private val mAnimationDurationTime: Long = 300
private val mAnimationSource = PointF()
private var mAnimationStartTime: Long = 0
private val mAnimationTarget = PointF()
private var mAnimationTargetEvent: Int = 0
private val mCurlDir = PointF()
private val mCurlPos = PointF()
private var mCurlState = CURL_NONE
// Current bitmap index. This is always showed as front of right page.
private var mCurrentIndex = 0
// Start position for dragging.
private val mDragStartPos = PointF()
private var mEnableTouchPressure = false
// Bitmap size. These are updated from renderer once it's initialized.
private var mPageBitmapHeight = -1
private var mPageBitmapWidth = -1
// Page meshes. Left and right meshes are 'static' while curl is used to
// show page flipping.
private var mPageCurl: CurlMesh
private var mPageLeft: CurlMesh
private var mPageRight: CurlMesh
private val mPointerPos = PointerPosition()
private var mRenderer: CurlRenderer = CurlRenderer(this)
private var mRenderLeftPage = true
private var mSizeChangedObserver: SizeChangedObserver? = null
// One page is the default.
private var mViewMode = SHOW_ONE_PAGE
var mPageProvider: PageProvider? = null
set(value) {
field = value
mCurrentIndex = 0
updatePages()
requestRender()
}
/**
* Get current page index. Page indices are zero based values presenting
* page being shown on right side of the book.
*/
/**
* Set current page index. Page indices are zero based values presenting
* page being shown on right side of the book. E.g if you set value to 4;
* right side front facing bitmap will be with index 4, back facing 5 and
* for left side page index 3 is front facing, and index 2 back facing (once
* page is on left side it's flipped over).
*
*
* Current index is rounded to closest value divisible with 2.
*/
var currentIndex: Int
get() = mCurrentIndex
set(index) {
mCurrentIndex = if (mPageProvider == null || index < 0) {
0
} else {
if (mAllowLastPageCurl) {
min(index, mPageProvider!!.pageCount)
} else {
min(index, mPageProvider!!.pageCount - 1)
}
}
updatePages()
requestRender()
}
/**
* Default constructor.
*/
constructor(ctx: Context) : super(ctx)
/**
* Default constructor.
*/
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
/**
* Default constructor.
*/
constructor(ctx: Context, attrs: AttributeSet, defStyle: Int) : this(ctx, attrs)
/**
* Initialize method.
*/
init {
setEGLConfigChooser(8, 8, 8, 8, 16, 0)
holder.setFormat(PixelFormat.TRANSLUCENT)
setZOrderOnTop(true)
setRenderer(mRenderer)
renderMode = RENDERMODE_WHEN_DIRTY
setOnTouchListener(this)
// Even though left and right pages are static we have to allocate room
// for curl on them too as we are switching meshes. Another way would be
// to swap texture ids only.
mPageLeft = CurlMesh(10)
mPageRight = CurlMesh(10)
mPageCurl = CurlMesh(10)
mPageLeft.setFlipTexture(true)
mPageRight.setFlipTexture(false)
}
override var canDraw: Boolean = false
override fun onDrawFrame() {
// We are not animating.
if (!mAnimate) {
return
}
val currentTime = System.currentTimeMillis()
// If animation is done.
if (currentTime >= mAnimationStartTime + mAnimationDurationTime) {
if (mAnimationTargetEvent == SET_CURL_TO_RIGHT) {
// Switch curled page to right.
val right = mPageCurl
val curl = mPageRight
right.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!)
right.setFlipTexture(false)
right.reset()
mRenderer.removeCurlMesh(curl)
mPageCurl = curl
mPageRight = right
// If we were curling left page update current index.
if (mCurlState == CURL_LEFT) {
--mCurrentIndex
callBack?.pageChange(-1)
}
canDraw = false
} else if (mAnimationTargetEvent == SET_CURL_TO_LEFT) {
// Switch curled page to left.
val left = mPageCurl
val curl = mPageLeft
left.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
left.setFlipTexture(true)
left.reset()
mRenderer.removeCurlMesh(curl)
if (!mRenderLeftPage) {
mRenderer.removeCurlMesh(left)
}
mPageCurl = curl
mPageLeft = left
// If we were curling right page update current index.
if (mCurlState == CURL_RIGHT) {
++mCurrentIndex
callBack?.pageChange(1)
}
canDraw = false
}
mCurlState = CURL_NONE
mAnimate = false
requestRender()
} else {
mPointerPos.mPos.set(mAnimationSource)
var t = 1f - (currentTime - mAnimationStartTime).toFloat() / mAnimationDurationTime
t = 1f - t * t * t * (3 - 2 * t)
mPointerPos.mPos.x += (mAnimationTarget.x - mAnimationSource.x) * t
mPointerPos.mPos.y += (mAnimationTarget.y - mAnimationSource.y) * t
updateCurlPos(mPointerPos)
}
}
override fun onPageSizeChanged(width: Int, height: Int) {
mPageBitmapWidth = width
mPageBitmapHeight = height
updatePages()
requestRender()
}
public override fun onSizeChanged(w: Int, h: Int, ow: Int, oh: Int) {
super.onSizeChanged(w, h, ow, oh)
requestRender()
if (mSizeChangedObserver != null) {
mSizeChangedObserver!!.onSizeChanged(w, h)
}
}
override fun onSurfaceCreated() {
// In case surface is recreated, let page meshes drop allocated texture
// ids and ask for new ones. There's no need to set textures here as
// onPageSizeChanged should be called later on.
mPageLeft.resetTexture()
mPageRight.resetTexture()
mPageCurl.resetTexture()
}
override fun onTouch(view: View, me: MotionEvent): Boolean {
// No dragging during animation at the moment.
if (mAnimate || mPageProvider == null) {
return false
}
// We need page rects quite extensively so get them for later use.
val rightRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
val leftRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)
// Store pointer position.
mPointerPos.mPos.set(me.x, me.y)
mRenderer.translate(mPointerPos.mPos)
if (mEnableTouchPressure) {
mPointerPos.mPressure = me.pressure
} else {
mPointerPos.mPressure = 0.8f
}
when (me.action) {
MotionEvent.ACTION_DOWN -> {
run {
// Once we receive pointer down event its position is mapped to
// right or left edge of page and that'll be the position from where
// user is holding the paper to make curl happen.
mDragStartPos.set(mPointerPos.mPos)
// First we make sure it's not over or below page. Pages are
// supposed to be same height so it really doesn't matter do we use
// left or right one.
if (mDragStartPos.y > rightRect!!.top) {
mDragStartPos.y = rightRect.top
} else if (mDragStartPos.y < rightRect.bottom) {
mDragStartPos.y = rightRect.bottom
}
// Then we have to make decisions for the user whether curl is going
// to happen from left or right, and on which page.
if (mViewMode == SHOW_TWO_PAGES) {
// If we have an open book and pointer is on the left from right
// page we'll mark drag position to left edge of left page.
// Additionally checking mCurrentIndex is higher than zero tells
// us there is a visible page at all.
if (mDragStartPos.x < rightRect.left && mCurrentIndex > 0) {
mDragStartPos.x = leftRect!!.left
startCurl(CURL_LEFT)
} else if (mDragStartPos.x >= rightRect.left && mCurrentIndex < mPageProvider!!.pageCount) {
mDragStartPos.x = rightRect.right
if (!mAllowLastPageCurl && mCurrentIndex >= mPageProvider!!.pageCount - 1) {
return false
}
startCurl(CURL_RIGHT)
}// Otherwise check pointer is on right page's side.
} else if (mViewMode == SHOW_ONE_PAGE) {
val halfX = (rightRect.right + rightRect.left) / 2
if (mDragStartPos.x < halfX && mCurrentIndex > 0) {
mDragStartPos.x = rightRect.left
startCurl(CURL_LEFT)
} else if (mDragStartPos.x >= halfX && mCurrentIndex < mPageProvider!!.pageCount) {
mDragStartPos.x = rightRect.right
if (!mAllowLastPageCurl && mCurrentIndex >= mPageProvider!!.pageCount - 1) {
return false
}
startCurl(CURL_RIGHT)
}
}
// If we have are in curl state, let this case clause flow through
// to next one. We have pointer position and drag position defined
// and this will create first render request given these points.
if (mCurlState == CURL_NONE) {
return false
}
}
updateCurlPos(mPointerPos)
}
MotionEvent.ACTION_MOVE -> {
updateCurlPos(mPointerPos)
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (mCurlState == CURL_LEFT || mCurlState == CURL_RIGHT) {
// Animation source is the point from where animation starts.
// Also it's handled in a way we actually simulate touch events
// meaning the output is exactly the same as if user drags the
// page to other side. While not producing the best looking
// result (which is easier done by altering curl position and/or
// direction directly), this is done in a hope it made code a
// bit more readable and easier to maintain.
mAnimationSource.set(mPointerPos.mPos)
mAnimationStartTime = System.currentTimeMillis()
// Given the explanation, here we decide whether to simulate
// drag to left or right end.
if (mViewMode == SHOW_ONE_PAGE && mPointerPos.mPos.x > (rightRect!!.left + rightRect.right) / 2 || mViewMode == SHOW_TWO_PAGES && mPointerPos.mPos.x > rightRect!!.left) {
// On right side target is always right page's right border.
mAnimationTarget.set(mDragStartPos)
mAnimationTarget.x = mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT)!!.right
mAnimationTargetEvent = SET_CURL_TO_RIGHT
} else {
// On left side target depends on visible pages.
mAnimationTarget.set(mDragStartPos)
if (mCurlState == CURL_RIGHT || mViewMode == SHOW_TWO_PAGES) {
mAnimationTarget.x = leftRect!!.left
} else {
mAnimationTarget.x = rightRect!!.left
}
mAnimationTargetEvent = SET_CURL_TO_LEFT
}
mAnimate = true
requestRender()
}
}
}
return true
}
/**
* Allow the last page to curl.
*/
fun setAllowLastPageCurl(allowLastPageCurl: Boolean) {
mAllowLastPageCurl = allowLastPageCurl
}
/**
* Sets mPageCurl curl position.
*/
private fun setCurlPos(curlPos: PointF, curlDir: PointF, radius: Double) {
// First reposition curl so that page doesn't 'rip off' from book.
if (mCurlState == CURL_RIGHT || mCurlState == CURL_LEFT && mViewMode == SHOW_ONE_PAGE) {
val pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
if (curlPos.x >= pageRect!!.right) {
mPageCurl.reset()
requestRender()
return
}
if (curlPos.x < pageRect.left) {
curlPos.x = pageRect.left
}
if (curlDir.y != 0f) {
val diffX = curlPos.x - pageRect.left
val leftY = curlPos.y + diffX * curlDir.x / curlDir.y
if (curlDir.y < 0 && leftY < pageRect.top) {
curlDir.x = curlPos.y - pageRect.top
curlDir.y = pageRect.left - curlPos.x
} else if (curlDir.y > 0 && leftY > pageRect.bottom) {
curlDir.x = pageRect.bottom - curlPos.y
curlDir.y = curlPos.x - pageRect.left
}
}
} else if (mCurlState == CURL_LEFT) {
val pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)
if (curlPos.x <= pageRect!!.left) {
mPageCurl.reset()
requestRender()
return
}
if (curlPos.x > pageRect.right) {
curlPos.x = pageRect.right
}
if (curlDir.y != 0f) {
val diffX = curlPos.x - pageRect.right
val rightY = curlPos.y + diffX * curlDir.x / curlDir.y
if (curlDir.y < 0 && rightY < pageRect.top) {
curlDir.x = pageRect.top - curlPos.y
curlDir.y = curlPos.x - pageRect.right
} else if (curlDir.y > 0 && rightY > pageRect.bottom) {
curlDir.x = curlPos.y - pageRect.bottom
curlDir.y = pageRect.right - curlPos.x
}
}
}
// Finally normalize direction vector and do rendering.
val dist = sqrt((curlDir.x * curlDir.x + curlDir.y * curlDir.y).toDouble())
if (dist != 0.0) {
curlDir.x /= dist.toFloat()
curlDir.y /= dist.toFloat()
mPageCurl.curl(curlPos, curlDir, radius)
} else {
mPageCurl.reset()
}
requestRender()
}
/**
* If set to true, touch event pressure information is used to adjust curl
* radius. The more you press, the flatter the curl becomes. This is
* somewhat experimental and results may vary significantly between devices.
* On emulator pressure information seems to be flat 1.0f which is maximum
* value and therefore not very much of use.
*/
fun setEnableTouchPressure(enableTouchPressure: Boolean) {
mEnableTouchPressure = enableTouchPressure
}
/**
* Setter for whether left side page is rendered. This is useful mostly for
* situations where right (main) page is aligned to left side of screen and
* left page is not visible anyway.
*/
fun setRenderLeftPage(renderLeftPage: Boolean) {
mRenderLeftPage = renderLeftPage
}
/**
* Sets SizeChangedObserver for this View. Call back method is called from
* this View's onSizeChanged method.
*/
fun setSizeChangedObserver(observer: SizeChangedObserver) {
mSizeChangedObserver = observer
}
/**
* Sets view mode. Value can be either SHOW_ONE_PAGE or SHOW_TWO_PAGES. In
* former case right page is made size of display, and in latter case two
* pages are laid on visible area.
*/
fun setViewMode(viewMode: Int) {
when (viewMode) {
SHOW_ONE_PAGE -> {
mViewMode = viewMode
mPageLeft.setFlipTexture(true)
mRenderer.setViewMode(CurlRenderer.SHOW_ONE_PAGE)
}
SHOW_TWO_PAGES -> {
mViewMode = viewMode
mPageLeft.setFlipTexture(false)
mRenderer.setViewMode(CurlRenderer.SHOW_TWO_PAGES)
}
}
}
/**
* Switches meshes and loads new bitmaps if available. Updated to support 2
* pages in landscape
*/
private fun startCurl(page: Int) {
when (page) {
// Once right side page is curled, first right page is assigned into
// curled page. And if there are more bitmaps available new bitmap is
// loaded into right side mesh.
CURL_RIGHT -> {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
// We are curling right page.
val curl = mPageRight
mPageRight = mPageCurl
mPageCurl = curl
if (mCurrentIndex > 0) {
mPageLeft.setFlipTexture(true)
mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageLeft.reset()
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft)
}
}
if (mCurrentIndex < mPageProvider!!.pageCount - 1) {
updatePage(mPageRight.texturePage, mCurrentIndex + 1)
mPageRight.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
mPageRight.setFlipTexture(false)
mPageRight.reset()
mRenderer.addCurlMesh(mPageRight)
}
// Add curled page to renderer.
mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!)
mPageCurl.setFlipTexture(false)
mPageCurl.reset()
mRenderer.addCurlMesh(mPageCurl)
mCurlState = CURL_RIGHT
}
// On left side curl, left page is assigned to curled page. And if
// there are more bitmaps available before currentIndex, new bitmap
// is loaded into left page.
CURL_LEFT -> {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
// We are curling left page.
val curl = mPageLeft
mPageLeft = mPageCurl
mPageCurl = curl
if (mCurrentIndex > 1) {
updatePage(mPageLeft.texturePage, mCurrentIndex - 2)
mPageLeft.setFlipTexture(true)
mPageLeft
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageLeft.reset()
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft)
}
}
// If there is something to show on right page add it to renderer.
if (mCurrentIndex < mPageProvider!!.pageCount) {
mPageRight.setFlipTexture(false)
mPageRight.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
mPageRight.reset()
mRenderer.addCurlMesh(mPageRight)
}
// How dragging previous page happens depends on view mode.
if (mViewMode == SHOW_ONE_PAGE || mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES) {
mPageCurl.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
mPageCurl.setFlipTexture(false)
} else {
mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageCurl.setFlipTexture(true)
}
mPageCurl.reset()
mRenderer.addCurlMesh(mPageCurl)
mCurlState = CURL_LEFT
}
}
}
/**
* Updates curl position.
*/
private fun updateCurlPos(pointerPos: PointerPosition) {
// Default curl radius.
var radius = (mRenderer.getPageRect(CURL_RIGHT)!!.width() / 3).toDouble()
// TODO: This is not an optimal solution. Based on feedback received so
// far; pressure is not very accurate, it may be better not to map
// coefficient to range [0f, 1f] but something like [.2f, 1f] instead.
// Leaving it as is until get my hands on a real device. On emulator
// this doesn't work anyway.
radius *= max(1f - pointerPos.mPressure, 0f).toDouble()
// NOTE: Here we set pointerPos to mCurlPos. It might be a bit confusing
// later to see e.g "mCurlPos.x - mDragStartPos.x" used. But it's
// actually pointerPos we are doing calculations against. Why? Simply to
// optimize code a bit with the cost of making it unreadable. Otherwise
// we had to this in both of the next if-else branches.
mCurlPos.set(pointerPos.mPos)
// If curl happens on right page, or on left page on two page mode,
// we'll calculate curl position from pointerPos.
if (mCurlState == CURL_RIGHT || mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES) {
mCurlDir.x = mCurlPos.x - mDragStartPos.x
mCurlDir.y = mCurlPos.y - mDragStartPos.y
val dist =
sqrt((mCurlDir.x * mCurlDir.x + mCurlDir.y * mCurlDir.y).toDouble()).toFloat()
// Adjust curl radius so that if page is dragged far enough on
// opposite side, radius gets closer to zero.
val pageWidth = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
.width()
var curlLen = radius * Math.PI
if (dist > pageWidth * 2 - curlLen) {
curlLen = max(pageWidth * 2 - dist, 0f).toDouble()
radius = curlLen / Math.PI
}
// Actual curl position calculation.
if (dist >= curlLen) {
val translate = (dist - curlLen) / 2
if (mViewMode == SHOW_TWO_PAGES) {
mCurlPos.x -= (mCurlDir.x * translate / dist).toFloat()
} else {
val pageLeftX = mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT)!!.left
radius = max(
min((mCurlPos.x - pageLeftX).toDouble(), radius),
0.0
)
}
mCurlPos.y -= (mCurlDir.y * translate / dist).toFloat()
} else {
val angle = Math.PI * sqrt(dist / curlLen)
val translate = radius * sin(angle)
mCurlPos.x += (mCurlDir.x * translate / dist).toFloat()
mCurlPos.y += (mCurlDir.y * translate / dist).toFloat()
}
} else if (mCurlState == CURL_LEFT) {
// Adjust radius regarding how close to page edge we are.
val pageLeftX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!.left
radius = max(min((mCurlPos.x - pageLeftX).toDouble(), radius), 0.0)
val pageRightX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!.right
mCurlPos.x -= min((pageRightX - mCurlPos.x).toDouble(), radius).toFloat()
mCurlDir.x = mCurlPos.x + mDragStartPos.x
mCurlDir.y = mCurlPos.y - mDragStartPos.y
}// Otherwise we'll let curl follow pointer position.
setCurlPos(mCurlPos, mCurlDir, radius)
}
/**
* Updates given CurlPage via PageProvider for page located at index.
*/
private fun updatePage(page: CurlPage, index: Int) {
// First reset page to initial state.
page.reset()
// Ask page provider to fill it up with bitmaps and colors.
mPageProvider!!.updatePage(
page, mPageBitmapWidth, mPageBitmapHeight,
index
)
}
/**
* Updates bitmaps for page meshes.
*/
fun updatePages() {
if (mPageProvider == null || mPageBitmapWidth <= 0
|| mPageBitmapHeight <= 0
) {
return
}
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
var leftIdx = mCurrentIndex - 1
var rightIdx = mCurrentIndex
var curlIdx = -1
if (mCurlState == CURL_LEFT) {
curlIdx = leftIdx
--leftIdx
} else if (mCurlState == CURL_RIGHT) {
curlIdx = rightIdx
++rightIdx
}
if (rightIdx >= 0 && rightIdx < mPageProvider!!.pageCount) {
updatePage(mPageRight.texturePage, rightIdx)
mPageRight.setFlipTexture(false)
mPageRight.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!)
mPageRight.reset()
mRenderer.addCurlMesh(mPageRight)
}
if (leftIdx >= 0 && leftIdx < mPageProvider!!.pageCount) {
updatePage(mPageLeft.texturePage, leftIdx)
mPageLeft.setFlipTexture(true)
mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageLeft.reset()
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft)
}
}
if (curlIdx >= 0 && curlIdx < mPageProvider!!.pageCount) {
updatePage(mPageCurl.texturePage, curlIdx)
if (mCurlState == CURL_RIGHT) {
mPageCurl.setFlipTexture(true)
mPageCurl.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
} else {
mPageCurl.setFlipTexture(false)
mPageCurl
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
}
mPageCurl.reset()
mRenderer.addCurlMesh(mPageCurl)
}
}
/**
* Provider for feeding 'book' with bitmaps which are used for rendering
* pages.
*/
interface PageProvider {
/**
* Return number of pages available.
*/
val pageCount: Int
/**
* Called once new bitmaps/textures are needed. Width and height are in
* pixels telling the size it will be drawn on screen and following them
* ensures that aspect ratio remains. But it's possible to return bitmap
* of any size though. You should use provided CurlPage for storing page
* information for requested page number.<br></br>
* <br></br>
* Index is a number between 0 and getBitmapCount() - 1.
*/
fun updatePage(page: CurlPage, width: Int, height: Int, index: Int)
}
/**
* Simple holder for pointer position.
*/
private inner class PointerPosition {
internal var mPos = PointF()
internal var mPressure: Float = 0.toFloat()
}
/**
* Observer interface for handling CurlView size changes.
*/
interface SizeChangedObserver {
/**
* Called once CurlView size changes.
*/
fun onSizeChanged(width: Int, height: Int)
}
interface CallBack {
fun pageChange(change: Int)
}
companion object {
// Curl state. We are flipping none, left or right page.
private const val CURL_LEFT = 1
private const val CURL_NONE = 0
private const val CURL_RIGHT = 2
// Constants for mAnimationTargetEvent.
private const val SET_CURL_TO_LEFT = 1
private const val SET_CURL_TO_RIGHT = 2
// Shows one page at the center of view.
const val SHOW_ONE_PAGE = 1
// Shows two pages side by side.
const val SHOW_TWO_PAGES = 2
}
}

@ -1,126 +0,0 @@
package io.legado.app.ui.widget.page.delegate
import android.graphics.Canvas
import android.view.MotionEvent
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.widget.page.curl.CurlPage
import io.legado.app.ui.widget.page.curl.CurlView
import io.legado.app.utils.screenshot
import kotlin.math.abs
class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView),
CurlView.CallBack {
var curlView: CurlView? = null
init {
pageView.curlView ?: let {
curlView = CurlView(pageView.context)
pageView.curlView = curlView
pageView.addView(curlView)
curlView?.mPageProvider = PageProvider()
curlView?.setSizeChangedObserver(SizeChangedObserver())
curlView?.callBack = this
}
}
override fun onTouch(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
curlView?.currentIndex = 1
}
}
curlView?.dispatchTouchEvent(event)
return super.onTouch(event)
}
override fun onScrollStart() {
}
override fun onDraw(canvas: Canvas) {
}
override fun onScrollStop() {
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!isMoved) {
val event = e1.toAction(MotionEvent.ACTION_UP)
curPage?.dispatchTouchEvent(event)
event.recycle()
if (abs(distanceX) > abs(distanceY)) {
if (distanceX < 0) {
//如果上一页不存在
if (!hasPrev()) {
noNext = true
return true
}
//上一页截图
bitmap = prevPage?.screenshot()
} else {
//如果不存在表示没有下一页了
if (!hasNext()) {
noNext = true
return true
}
//下一页截图
bitmap = nextPage?.screenshot()
}
isMoved = true
}
}
if (isMoved) {
curlView?.canDraw = true
isCancel = if (pageView.isScrollDelegate) {
if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0
} else {
if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0
}
isRunning = true
//设置触摸点
setTouchPoint(e2.x, e2.y)
}
return isMoved
}
override fun onPageUp() {
curlView?.updatePages()
curlView?.requestRender()
}
override fun pageChange(change: Int) {
pageView.post {
if (change > 0) {
pageView.moveToNextPage()
} else {
pageView.moveToPrevPage()
}
}
}
private inner class PageProvider : CurlView.PageProvider {
override val pageCount: Int
get() = 3
override fun updatePage(page: CurlPage, width: Int, height: Int, index: Int) {
when (index) {
0 -> page.setTexture(prevPage?.screenshot(), CurlPage.SIDE_BOTH)
1 -> page.setTexture(curPage?.screenshot(), CurlPage.SIDE_BOTH)
2 -> page.setTexture(nextPage?.screenshot(), CurlPage.SIDE_BOTH)
}
}
}
// 定义书籍尺寸的变化监听器
private inner class SizeChangedObserver : CurlView.SizeChangedObserver {
override fun onSizeChanged(width: Int, height: Int) {
curlView?.setViewMode(CurlView.SHOW_ONE_PAGE)
}
}
}

@ -1,32 +1,36 @@
package io.legado.app.utils
import android.content.Context
import android.graphics.PorterDuff
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl
import androidx.core.view.forEach
import io.legado.app.R
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.DrawableUtils
import io.legado.app.lib.theme.primaryTextColor
import java.lang.reflect.Method
import java.util.*
fun Menu.applyTint(context: Context, inPrimary: Boolean = true): Menu = this.let { menu ->
fun Menu.applyTint(context: Context, theme: Theme = Theme.Auto): Menu = this.let { menu ->
if (menu is MenuBuilder) {
menu.setOptionalIconsVisible(true)
}
val primaryTextColor = context.primaryTextColor
val defaultTextColor = context.getCompatColor(R.color.tv_text_default)
val tintColor = when (theme) {
Theme.Dark -> context.getCompatColor(R.color.md_white_1000)
Theme.Light -> context.getCompatColor(R.color.md_black_1000)
else -> primaryTextColor
}
menu.forEach { item ->
(item as MenuItemImpl).let { impl ->
//overflow:展开的item
DrawableUtils.setTint(
impl.icon,
if (!inPrimary) defaultTextColor
else if (impl.requiresOverflow()) defaultTextColor
else primaryTextColor
if (impl.requiresOverflow()) defaultTextColor
else tintColor
)
}
}
@ -47,14 +51,10 @@ fun Menu.applyOpenTint(context: Context) {
if (menuItems is ArrayList<*>) {
for (menuItem in menuItems) {
if (menuItem is MenuItem) {
val drawable = menuItem.icon
if (drawable != null) {
drawable.mutate()
drawable.setColorFilter(
defaultTextColor,
PorterDuff.Mode.SRC_ATOP
)
}
DrawableUtils.setTint(
menuItem.icon,
defaultTextColor
)
}
}
}

@ -71,7 +71,9 @@ class SourceController {
fun deleteSources(postData: String?): ReturnData {
kotlin.runCatching {
GSON.fromJsonArray<BookSource>(postData)?.let {
App.db.bookSourceDao().delete(*it.toTypedArray())
it.forEach { source ->
App.db.bookSourceDao().delete(source)
}
}
}
return ReturnData().setData("已执行"/*okSources*/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17000000"
android:endColor="#00000000"
android:angle="90" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17FFFFFF"
android:endColor="#00FFFFFF"
android:angle="90" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17000000"
android:endColor="#00000000"
android:angle="270" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17FFFFFF"
android:endColor="#00FFFFFF"
android:angle="270" />
</shape>

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<color xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/item_bg_color" />

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<color xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/md_grey_100" />

@ -1,9 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>

@ -15,20 +15,18 @@
android:scaleType="centerCrop"
tools:ignore="ContentDescription" />
<View
android:id="@+id/vw_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#50000000" />
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/vw_bg"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@drawable/bg_gradient"
app:layout_constraintTop_toBottomOf="@+id/title_bar"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/tv_speed"
android:layout_width="wrap_content"

@ -6,6 +6,23 @@
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/bg_book"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:contentDescription="@string/bg_image"
app:layout_constraintBottom_toTopOf="@+id/view_info"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/vw_bg"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#50000000"
app:layout_constraintBottom_toTopOf="@+id/view_info"
app:layout_constraintTop_toTopOf="parent" />
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
@ -42,7 +59,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:singleLine="true"
android:textColor="@color/tv_text_default"
android:textColor="@color/md_white_1000"
android:textSize="18sp" />
<TextView
@ -51,6 +68,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:singleLine="true"
android:textColor="@color/md_white_1000"
android:textSize="16sp" />
<LinearLayout
@ -66,6 +84,7 @@
android:layout_weight="1"
android:singleLine="true"
android:textSize="16sp"
android:textColor="@color/md_white_1000"
tools:ignore="NestedWeights" />
<io.legado.app.lib.theme.view.ATEAccentBgTextView
@ -133,6 +152,7 @@
android:layout_marginBottom="4dp"
android:singleLine="true"
android:textSize="16sp"
android:textColor="@color/md_white_1000"
app:layout_constraintBottom_toBottomOf="@+id/iv_cover"
app:layout_constraintLeft_toLeftOf="@+id/tv_name"
app:layout_constraintRight_toRightOf="@id/tv_name" />
@ -145,8 +165,8 @@
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/iv_cover"
app:layout_constraintBottom_toTopOf="@+id/view_other">
app:layout_constraintBottom_toTopOf="@+id/view_other"
app:layout_constraintTop_toBottomOf="@+id/iv_cover">
<View
android:layout_width="match_parent"
@ -189,8 +209,8 @@
android:orientation="vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp"
app:layout_constraintTop_toBottomOf="@+id/view_info"
app:layout_constraintBottom_toTopOf="@+id/fl_action">
app:layout_constraintBottom_toTopOf="@+id/fl_action"
app:layout_constraintTop_toBottomOf="@+id/view_info">
<TextView
android:id="@+id/tv_group"

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save