Merge pull request #30 from gedoor/master

update
pull/481/head
Antecer 6 years ago committed by GitHub
commit c8cae859f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 43
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  2. 2
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  3. 17
      app/src/main/java/io/legado/app/data/entities/Book.kt
  4. 4
      app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt
  5. 2
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  6. 4
      app/src/main/java/io/legado/app/data/entities/Source.kt
  7. 67
      app/src/main/java/io/legado/app/help/LayoutManager.kt
  8. 32
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  9. 5
      app/src/main/java/io/legado/app/ui/main/MainModel.kt
  10. 18
      app/src/main/java/io/legado/app/ui/search/SearchActivity.kt
  11. 6
      app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt
  12. 67
      app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
  13. 221
      app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt
  14. 29
      app/src/main/java/io/legado/app/ui/widget/dynamiclayout/ViewSwitcher.kt
  15. 6
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  16. 45
      app/src/main/res/layout/activity_main.xml
  17. 47
      app/src/main/res/layout/activity_search.xml
  18. 20
      app/src/main/res/layout/view_dynamic.xml
  19. 27
      app/src/main/res/layout/view_error.xml
  20. 19
      app/src/main/res/layout/view_loading.xml
  21. 14
      app/src/main/res/layout/view_titlebar.xml
  22. 40
      app/src/main/res/values/attrs.xml
  23. 2
      app/src/main/res/values/colors.xml
  24. 5
      app/src/main/res/values/dimens.xml
  25. 5
      app/src/main/res/values/strings.xml
  26. 26
      app/src/main/res/values/styles.xml

@ -2,17 +2,28 @@ package io.legado.app.base
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModel
abstract class BaseActivity : AppCompatActivity() { abstract class BaseActivity<BD : ViewDataBinding, VM : ViewModel> : AppCompatActivity() {
@LayoutRes protected lateinit var dataBinding: BD
abstract fun getLayoutID(): Int private set
protected abstract val viewModel: VM
protected abstract val layoutID: Int
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(getLayoutID()) dataBinding = DataBindingUtil.setContentView(this, layoutID)
onViewModelCreated(viewModel, savedInstanceState)
}
open fun onViewModelCreated(viewModel: VM, savedInstanceState: Bundle?){
} }
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem?): Boolean {
@ -22,7 +33,7 @@ abstract class BaseActivity : AppCompatActivity() {
return true return true
} }
} }
return if (item == null) true else onCompatOptionsItemSelected(item) return if (item == null) false else onCompatOptionsItemSelected(item)
} }
open fun onCompatOptionsItemSelected(item: MenuItem): Boolean { open fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
@ -30,26 +41,18 @@ abstract class BaseActivity : AppCompatActivity() {
} }
override fun setTitle(title: CharSequence?) { override fun setTitle(title: CharSequence?) {
supportActionBar?.let { supportActionBar?.title = title
it.title = title
}
} }
override fun setTitle(titleId: Int) { override fun setTitle(titleId: Int) {
supportActionBar?.let { supportActionBar?.setTitle(titleId)
it.setTitle(titleId)
}
} }
fun setSubTitle(subtitle: CharSequence?){ fun setSubTitle(subtitle: CharSequence?) {
supportActionBar?.let { supportActionBar?.subtitle = subtitle
it.subtitle = subtitle;
}
} }
fun setSubTitle(subtitleId: Int){ fun setSubTitle(subtitleId: Int) {
supportActionBar?.let { supportActionBar?.setSubtitle(subtitleId)
it.setSubtitle(subtitleId)
}
} }
} }

@ -13,7 +13,7 @@ import io.legado.app.data.entities.ReplaceRule
import javax.xml.transform.Source import javax.xml.transform.Source
@Database(entities = [Book::class, Chapter::class, ReplaceRule::class, Source::class], version = 1, exportSchema = true) @Database(entities = [Book::class, Chapter::class, ReplaceRule::class], version = 1, exportSchema = true)
// @TypeConverters(Converters::class) // @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {

@ -2,15 +2,17 @@ package io.legado.app.data.entities
import android.os.Parcelable import android.os.Parcelable
import androidx.room.* import androidx.room.*
import io.legado.app.utils.strim
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@Entity(tableName = "books", indices = [(Index(value = ["descUrl"]))]) @Entity(tableName = "books", indices = [(Index(value = ["descUrl"], unique = true))])
data class Book(@PrimaryKey data class Book(@PrimaryKey
var descUrl: String = "", // 详情页Url(本地书源存储完整文件路径) var descUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var sourceId: Int = -1, // 书源规则id(默认-1,表示本地书籍) var sourceId: Int = -1, // 书源规则id(默认-1,表示本地书籍)
var name: String = "", // 书籍名称(书源获取) var name: String = "", // 书籍名称(书源获取)
var customName: String = "", // 书籍名称(用户修改) var customName: String? = null, // 书籍名称(用户修改)
var author: String? = null, // 作者名称(书源获取) var author: String? = null, // 作者名称(书源获取)
var customAuthor: String? = null, // 作者名称(用户修改) var customAuthor: String? = null, // 作者名称(用户修改)
var tag: String? = null, // 分类信息(书源获取) var tag: String? = null, // 分类信息(书源获取)
@ -22,13 +24,12 @@ data class Book(@PrimaryKey
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // 0: 文本读物, 1: 有声读物 var type: Int = 0, // 0: 文本读物, 1: 有声读物
var group: Int = 0, // 自定义分组索引号 var group: Int = 0, // 自定义分组索引号
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = 0, // 最新章节标题更新时间 var latestChapterTime: Long = 0, // 最新章节标题更新时间
var lastCheckTime: Long = 0, // 最近一次更新书籍信息的时间 var lastCheckTime: Long = 0, // 最近一次更新书籍信息的时间
var lastCheckCount: Int = 0, // 最近一次发现新章节的数量 var lastCheckCount: Int = 0, // 最近一次发现新章节的数量
var totalChapterNum: Int = 0, // 书籍目录总数 var totalChapterNum: Int = 0, // 书籍目录总数
var durChapterTitle: String = "", // 当前章节名称 var durChapterTitle: String? = null, // 当前章节名称
var durChapterIndex: Int = 0, // 当前章节索引 var durChapterIndex: Int = 0, // 当前章节索引
var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置) var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置)
var durChapterTime: Long = 0, // 最近一次阅读书籍的时间(打开正文的时间) var durChapterTime: Long = 0, // 最近一次阅读书籍的时间(打开正文的时间)
@ -38,4 +39,12 @@ data class Book(@PrimaryKey
fun getUnreadChapterNum() = Math.max(totalChapterNum - durChapterIndex - 1, 0) fun getUnreadChapterNum() = Math.max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayName() = customName.strim() ?: name
fun getDisplayAuthor() = customAuthor.strim() ?: author
fun getDisplayCover() = customCoverUrl.strim() ?: coverUrl
fun getDisplayDescription() = customDescription.strim() ?: description
} }

@ -16,8 +16,8 @@ data class ReplaceRule(
val pattern: String? = null, val pattern: String? = null,
val replacement: String? = null, val replacement: String? = null,
val scope: String? = null, val scope: String? = null,
val isEnabled: Boolean? = null, val isEnabled: Boolean = true,
val isRegex: Boolean? = null, val isRegex: Boolean = true,
val order: Int = 0 val order: Int = 0
) : Parcelable ) : Parcelable

@ -0,0 +1,2 @@
package io.legado.app.data.entities

@ -14,14 +14,14 @@ data class Source(@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "sourceId") @ColumnInfo(name = "sourceId")
var id: Int = 0, // 编号 var id: Int = 0, // 编号
var name: String = "", // 名称 var name: String = "", // 名称
var origin: String = "", // 地址,包括 http/https var origin: String = "", // 地址,包括 http/https
var type: Int = 0, // 类型,0 文本,1 音频 var type: Int = 0, // 类型,0 文本,1 音频
var group: String? = null, // 分组 var group: String? = null, // 分组
var header: String? = null, // header var header: String? = null, // header
var loginUrl: String? = null, // 登录地址 var loginUrl: String? = null, // 登录地址
var isEnabled: Boolean = true, // 是否启用 var isEnabled: Boolean = true, // 是否启用
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var serialNumber: Int = 0, // 手动排序编号 var customOrder: Int = 0, // 手动排序编号
var weight: Int = 0, // 智能排序的权重 var weight: Int = 0, // 智能排序的权重
var exploreRule: String? = null, // 发现规则 var exploreRule: String? = null, // 发现规则
var searchRule: String? = null, // 搜索规则 var searchRule: String? = null, // 搜索规则

@ -0,0 +1,67 @@
package io.legado.app.help
import androidx.annotation.IntDef
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
class LayoutManager private constructor() {
interface LayoutManagerFactory {
fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager
}
@IntDef(LinearLayoutManager.HORIZONTAL, LinearLayoutManager.VERTICAL)
@Retention(AnnotationRetention.SOURCE)
annotation class Orientation
companion object {
fun linear(): LayoutManagerFactory {
return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return LinearLayoutManager(recyclerView.context)
}
}
}
fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory {
return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return LinearLayoutManager(recyclerView.context, orientation, reverseLayout)
}
}
}
fun grid(spanCount: Int): LayoutManagerFactory {
return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return GridLayoutManager(recyclerView.context, spanCount)
}
}
}
fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory {
return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout)
}
}
}
fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory {
return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return StaggeredGridLayoutManager(spanCount, orientation)
}
}
}
}
}

@ -1,28 +1,29 @@
package io.legado.app.ui.main package io.legado.app.ui.main
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProvider
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import io.legado.app.R import io.legado.app.R
import io.legado.app.utils.longSnackbar import io.legado.app.base.BaseActivity
import io.legado.app.ui.search.SearchActivity
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar_main.* import kotlinx.android.synthetic.main.app_bar_main.*
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : BaseActivity<MainDataBinding, MainViewModel>(), NavigationView.OnNavigationItemSelectedListener {
override val viewModel: MainViewModel
get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(MainViewModel::class.java)
override val layoutID: Int
get() = R.layout.activity_main
override fun onViewModelCreated(viewModel: MainViewModel, savedInstanceState: Bundle?) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
fab.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) }
fab.setOnClickListener { it.longSnackbar(R.string.app_name) }
val toggle = ActionBarDrawerToggle( val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, this, drawer_layout, toolbar,
@ -33,7 +34,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
toggle.syncState() toggle.syncState()
nav_view.setNavigationItemSelectedListener(this) nav_view.setNavigationItemSelectedListener(this)
val mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -51,14 +51,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will return super.onCompatOptionsItemSelected(item)
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
} }
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {

@ -1,5 +0,0 @@
package io.legado.app.ui.main
class MainModel {
}

@ -1,13 +1,21 @@
package io.legado.app.ui.search package io.legado.app.ui.search
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity
class SearchActivity : BaseActivity<SearchDataBinding, SearchViewModel>() {
override val viewModel: SearchViewModel
get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(SearchViewModel::class.java)
override val layoutID: Int
get() = R.layout.activity_search
override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) {
class SearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
} }
} }

@ -0,0 +1,6 @@
package io.legado.app.ui.search
import android.app.Application
import androidx.lifecycle.AndroidViewModel
class SearchViewModel(application: Application) : AndroidViewModel(application)

@ -1,24 +1,46 @@
package io.legado.app.ui.widget package io.legado.app.ui.widget
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.material.appbar.AppBarLayout
import io.legado.app.R import io.legado.app.R
import kotlinx.android.synthetic.main.view_titlebar.view.* import kotlinx.android.synthetic.main.view_titlebar.view.*
class TitleBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, attrs) {
init { init {
inflate(context, R.layout.view_titlebar, this) inflate(context, R.layout.view_titlebar, this)
} val a = context.obtainStyledAttributes(
attrs, R.styleable.TitleBar,
0, 0
)
val navigationIcon = a.getDrawable(R.styleable.TitleBar_navigationIcon)
val navigationContentDescription = a.getText(R.styleable.TitleBar_navigationContentDescription)
val navigationIconTint = a.getColorStateList(R.styleable.TitleBar_navigationIconTint)
val navigationIconTintMode = a.getInt(R.styleable.TitleBar_navigationIconTintMode, 9)
val showNavigationIcon = a.getBoolean(R.styleable.TitleBar_showNavigationIcon, true)
val attachToActivity = a.getBoolean(R.styleable.TitleBar_attachToActivity, true)
a.recycle()
override fun onAttachedToWindow() { if (showNavigationIcon) {
super.onAttachedToWindow() toolbar.apply {
attachToActivity() this.navigationIcon = navigationIcon
this.navigationContentDescription = navigationContentDescription
wrapDrawableTint(this.navigationIcon, navigationIconTint, navigationIconTintMode)
}
}
if (attachToActivity) {
attachToActivity(context)
}
} }
private fun attachToActivity(){ private fun attachToActivity(context: Context) {
val activity = getCompatActivity(context) val activity = getCompatActivity(context)
activity?.let { activity?.let {
activity.setSupportActionBar(toolbar) activity.setSupportActionBar(toolbar)
@ -37,4 +59,35 @@ class TitleBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, at
else -> null else -> null
} }
} }
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
}
}
} }

@ -0,0 +1,221 @@
package io.legado.app.ui.widget.dynamiclayout
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.R
import kotlinx.android.synthetic.main.view_dynamic.view.*
class DynamicFrameLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs), ViewSwitcher {
private var errorView: View? = null
private var errorImage: AppCompatImageView? = null
private var errorTextView: AppCompatTextView? = null
private var actionBtn: AppCompatButton? = null
private var progressView: View? = null
private var progressBar: ProgressBar? = null
private var contentView: View? = null
private var errorIcon: Drawable? = null
private var emptyIcon: Drawable? = null
private var errorActionDescription: CharSequence? = null
private var emptyActionDescription: CharSequence? = null
private var emptyDescription: CharSequence? = null
private var errorAction: Action? = null
private var emptyAction: Action? = null
private var changeListener: OnVisibilityChangeListener? = null
init {
View.inflate(context, R.layout.view_dynamic, this)
val a = context.obtainStyledAttributes(attrs, R.styleable.DynamicFrameLayout)
errorIcon = a.getDrawable(R.styleable.DynamicFrameLayout_errorSrc)
emptyIcon = a.getDrawable(R.styleable.DynamicFrameLayout_emptySrc)
emptyActionDescription = a.getText(R.styleable.DynamicFrameLayout_emptyActionDescription)
emptyDescription = a.getText(R.styleable.DynamicFrameLayout_emptyDescription)
errorActionDescription = a.getText(R.styleable.DynamicFrameLayout_errorActionDescription)
if (errorActionDescription == null) {
errorActionDescription = context.getString(R.string.dynamic_click_retry)
}
a.recycle()
}
override fun onFinishInflate() {
super.onFinishInflate()
if (childCount > 2) {
contentView = getChildAt(2)
}
}
override fun addView(child: View) {
if (childCount > 2) {
throw IllegalStateException("DynamicFrameLayout can host only one direct child")
}
super.addView(child)
}
override fun addView(child: View, index: Int) {
if (childCount > 2) {
throw IllegalStateException("DynamicFrameLayout can host only one direct child")
}
super.addView(child, index)
}
override fun addView(child: View, params: ViewGroup.LayoutParams) {
if (childCount > 2) {
throw IllegalStateException("DynamicFrameLayout can host only one direct child")
}
super.addView(child, params)
}
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) {
if (childCount > 2) {
throw IllegalStateException("DynamicFrameLayout can host only one direct child")
}
super.addView(child, index, params)
}
override fun showErrorView(message: CharSequence) {
ensureErrorView()
setViewVisible(errorView, true)
setViewVisible(contentView, false)
setViewVisible(progressView, false)
errorTextView?.text = message
errorImage?.setImageDrawable(errorIcon)
actionBtn?.let {
it.tag = ACTION_WHEN_ERROR
it.visibility = View.VISIBLE
if (errorActionDescription != null) {
it.text = errorActionDescription
}
}
dispatchVisibilityChanged(ViewSwitcher.SHOW_ERROR_VIEW)
}
override fun showErrorView(messageId: Int) {
showErrorView(resources.getText(messageId))
}
override fun showEmptyView() {
ensureErrorView()
setViewVisible(errorView, true)
setViewVisible(contentView, false)
setViewVisible(progressView, false)
errorTextView?.text = emptyDescription
errorImage?.setImageDrawable(emptyIcon)
actionBtn?.let {
it.tag = ACTION_WHEN_EMPTY
if (errorActionDescription != null) {
it.visibility = View.VISIBLE
it.text = errorActionDescription
} else {
it.visibility = View.INVISIBLE
}
}
dispatchVisibilityChanged(ViewSwitcher.SHOW_EMPTY_VIEW)
}
override fun showProgressView() {
ensureProgressView()
setViewVisible(errorView, false)
setViewVisible(contentView, false)
setViewVisible(progressView, true)
dispatchVisibilityChanged(ViewSwitcher.SHOW_PROGRESS_VIEW)
}
override fun showContentView() {
setViewVisible(errorView, false)
setViewVisible(contentView, true)
setViewVisible(progressView, false)
dispatchVisibilityChanged(ViewSwitcher.SHOW_CONTENT_VIEW)
}
fun setOnVisibilityChangeListener(listener: OnVisibilityChangeListener) {
changeListener = listener
}
fun setErrorAction(action: Action) {
errorAction = action
}
fun setEmptyAction(action: Action) {
emptyAction = action
}
private fun setViewVisible(view: View?, visible: Boolean) {
view?.let {
it.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
}
private fun ensureErrorView() {
if (errorView == null) {
errorView = errorViewStub.inflate()
errorImage = errorView?.findViewById(R.id.iv_error_image)
errorTextView = errorView?.findViewById(R.id.tv_error_message)
actionBtn = errorView?.findViewById(R.id.btn_error_retry)
actionBtn?.setOnClickListener {
when (it.tag) {
ACTION_WHEN_EMPTY -> emptyAction?.onAction(this@DynamicFrameLayout)
ACTION_WHEN_ERROR -> errorAction?.onAction(this@DynamicFrameLayout)
}
}
}
}
private fun ensureProgressView() {
if (progressView == null) {
progressView = progressViewStub.inflate()
progressBar = progressView?.findViewById(R.id.loading_progress)
}
}
private fun dispatchVisibilityChanged(@ViewSwitcher.Visibility visibility: Int) {
changeListener?.onVisibilityChanged(visibility)
}
interface Action {
fun onAction(switcher: ViewSwitcher)
}
interface OnVisibilityChangeListener {
fun onVisibilityChanged(@ViewSwitcher.Visibility visibility: Int)
}
companion object {
private const val ACTION_WHEN_ERROR = "ACTION_WHEN_ERROR"
private const val ACTION_WHEN_EMPTY = "ACTION_WHEN_EMPTY"
}
}

@ -0,0 +1,29 @@
package io.legado.app.ui.widget.dynamiclayout
import androidx.annotation.IntDef
import androidx.annotation.StringRes
interface ViewSwitcher {
companion object{
const val SHOW_CONTENT_VIEW = 0
const val SHOW_ERROR_VIEW = 1
const val SHOW_EMPTY_VIEW = 2
const val SHOW_PROGRESS_VIEW = 3
}
@Retention(AnnotationRetention.SOURCE)
@IntDef(SHOW_CONTENT_VIEW, SHOW_ERROR_VIEW, SHOW_EMPTY_VIEW, SHOW_PROGRESS_VIEW)
annotation class Visibility
fun showErrorView(message: CharSequence)
fun showErrorView(@StringRes messageId: Int)
fun showEmptyView()
fun showProgressView()
fun showContentView()
}

@ -0,0 +1,6 @@
package io.legado.app.utils
fun String?.strim() = if (this.isNullOrBlank()) null else this.trim()
fun String.isAbsUrl() = this.startsWith("http://", true)
|| this.startsWith("https://", true)

@ -1,26 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
xmlns:app="http://schemas.android.com/apk/res-auto" <data class=".ui.main.MainDataBinding">
xmlns:tools="http://schemas.android.com/tools" <variable name="MainViewModel" type="io.legado.app.ui.main.MainViewModel"/>
android:id="@+id/drawer_layout" </data>
android:layout_width="match_parent" <androidx.drawerlayout.widget.DrawerLayout
android:layout_height="match_parent" android:id="@+id/drawer_layout"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
layout="@layout/app_bar_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main" tools:openDrawer="start">
app:menu="@menu/activity_main_drawer"/>
<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer"/>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>
</layout>

@ -1,9 +1,40 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <layout xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" <data class=".ui.search.SearchDataBinding">
android:layout_width="match_parent" <variable name="SearchViewModel" type="io.legado.app.ui.search.SearchViewModel"/>
android:layout_height="match_parent" </data>
tools:context="io.legado.app.ui.search.SearchActivity">
<androidx.constraintlayout.widget.ConstraintLayout
</androidx.constraintlayout.widget.ConstraintLayout> xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="io.legado.app.ui.search.SearchActivity">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
/>
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleBar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchList"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/errorViewStub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout="@layout/view_error" />
<ViewStub
android:id="@+id/progressViewStub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout="@layout/view_loading" />
</merge>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_error_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_error_message"
style="@style/Style.Text.Primary.Normal"
android:paddingTop="16dp"
android:paddingBottom="16dp" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_error_retry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/colorAccent"
android:minWidth="96dp"
android:text="@string/dynamic_click_retry" />
</LinearLayout>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/loading_progress"
android:layout_width="48dp"
android:layout_height="48dp"
style="@style/Widget.AppCompat.ProgressBar"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_loading_message"
style="@style/Style.Text.Second.Normal.Wrap"
android:paddingTop="16dp"
android:text="@string/dynamic_loading"/>
</LinearLayout>

@ -1,16 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout <androidx.appcompat.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/appBar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"> app:popupTheme="@style/AppTheme.PopupOverlay"/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</com.google.android.material.appbar.AppBarLayout>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TitleBar">
<attr name="attachToActivity" format="boolean" />
<attr name="showNavigationIcon" format="boolean" />
<attr name="navigationIcon" format="reference" />
<attr name="navigationContentDescription" format="reference|string" />
<attr name="navigationIconTint" format="color|reference" />
<attr name="navigationIconTintMode" format="enum">
<enum name="clear" value="0" />
<enum name="src" value="1" />
<enum name="dst" value="2" />
<enum name="src_over" value="3" />
<enum name="dst_over" value="4" />
<enum name="src_in" value="5" />
<enum name="dst_in" value="6" />
<enum name="src_out" value="7" />
<enum name="dst_out" value="8" />
<enum name="src_atop" value="9" />
<enum name="dst_atop" value="10" />
<enum name="xor" value="11" />
<enum name="darken" value="16" />
<enum name="lighten" value="17" />
<enum name="multiply" value="13" />
<enum name="screen" value="14" />
<enum name="add" value="12" />
<enum name="overlay" value="15" />
</attr>
</declare-styleable>
<declare-styleable name="DynamicFrameLayout">
<attr name="errorSrc" format="reference" />
<attr name="emptySrc" format="reference" />
<attr name="errorActionDescription" format="string|reference" />
<attr name="emptyActionDescription" format="string|reference" />
<attr name="emptyDescription" format="string|reference" />
</declare-styleable>
</resources>

@ -3,4 +3,6 @@
<color name="colorPrimary">#008577</color> <color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color> <color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color> <color name="colorAccent">#D81B60</color>
</resources> </resources>

@ -5,4 +5,9 @@
<dimen name="nav_header_vertical_spacing">8dp</dimen> <dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen> <dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen> <dimen name="fab_margin">16dp</dimen>
<dimen name="font_size_normal">14sp</dimen>
<dimen name="font_size_middle">16sp</dimen>
<dimen name="font_size_large">18sp</dimen>
</resources> </resources>

@ -13,5 +13,8 @@
<string name="menu_tools">Tools</string> <string name="menu_tools">Tools</string>
<string name="menu_share">Share</string> <string name="menu_share">Share</string>
<string name="menu_send">Send</string> <string name="menu_send">Send</string>
<string name="action">Replace with your own action</string>
<string name="dynamic_click_retry">点击重试</string>
<string name="dynamic_loading">正在加载</string>
</resources> </resources>

@ -1,7 +1,7 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
@ -14,4 +14,28 @@
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/> <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/> <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
<style name="Style.Text.Primary.Normal" parent="android:Widget">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">@dimen/font_size_normal</item>
<item name="android:textColor">#000</item>
<item name="android:includeFontPadding">false</item>
</style>
<style name="Style.Text.Primary.Normal.Wrap">
<item name="android:layout_width">wrap_content</item>
</style>
<style name="Style.Text.Second.Normal" parent="android:Widget">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">@dimen/font_size_normal</item>
<item name="android:textColor">#676767</item>
<item name="android:includeFontPadding">false</item>
</style>
<style name="Style.Text.Second.Normal.Wrap">
<item name="android:layout_width">wrap_content</item>
</style>
</resources> </resources>

Loading…
Cancel
Save