Rename
This commit is contained in:
1 parent
b86d90021c
commit
6c0757c91b
972 files changed
+6885
-7321
No files matched your search
@@ -0,0 +1,99 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import io.legato.kazusa.constant.PreferKey
|
||||
import io.legato.kazusa.utils.getPrefInt
|
||||
import io.legato.kazusa.utils.getPrefString
|
||||
import io.legato.kazusa.utils.sysConfiguration
|
||||
import java.util.*
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
object AppContextWrapper {
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun wrap(context: Context): Context {
|
||||
val resources: Resources = context.resources
|
||||
val configuration: Configuration = resources.configuration
|
||||
val targetLocale = getSetLocale(context)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
configuration.setLocale(targetLocale)
|
||||
configuration.setLocales(LocaleList(targetLocale))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
configuration.locale = targetLocale
|
||||
}
|
||||
configuration.fontScale = getFontScale(context)
|
||||
return context.createConfigurationContext(configuration)
|
||||
}
|
||||
|
||||
fun getFontScale(context: Context): Float {
|
||||
var fontScale = context.getPrefInt(PreferKey.fontScale) / 10f
|
||||
if (fontScale !in 0.8f..1.6f) {
|
||||
fontScale = sysConfiguration.fontScale
|
||||
}
|
||||
return fontScale
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前系统语言
|
||||
*/
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun getSystemLocale(): Locale {
|
||||
val locale: Locale
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0有多语言设置获取顶部的语言
|
||||
locale = sysConfiguration.locales.get(0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
locale = sysConfiguration.locale
|
||||
}
|
||||
return locale
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前App语言
|
||||
*/
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun getAppLocale(context: Context): Locale {
|
||||
val locale: Locale
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
locale = context.resources.configuration.locales[0]
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
locale = context.resources.configuration.locale
|
||||
}
|
||||
return locale
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前设置语言
|
||||
*/
|
||||
private fun getSetLocale(context: Context): Locale {
|
||||
return when (context.getPrefString(PreferKey.language)) {
|
||||
"zh" -> Locale.SIMPLIFIED_CHINESE
|
||||
"tw" -> Locale.TRADITIONAL_CHINESE
|
||||
"en" -> Locale.ENGLISH
|
||||
else -> getSystemLocale()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断App语言和设置语言是否相同
|
||||
*/
|
||||
fun isSameWithSetting(context: Context): Boolean {
|
||||
val locale = getAppLocale(context)
|
||||
val language = locale.language
|
||||
val country = locale.country
|
||||
val pfLocale = getSetLocale(context)
|
||||
val pfLanguage = pfLocale.language
|
||||
val pfCountry = pfLocale.country
|
||||
return language == pfLanguage && country == pfCountry
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.addCallback
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppConst
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.constant.Theme
|
||||
import io.legato.kazusa.help.config.ThemeConfig
|
||||
//import io.legado.app.lib.theme.backgroundColor
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.utils.applyOpenTint
|
||||
import io.legato.kazusa.utils.applyTint
|
||||
import io.legato.kazusa.utils.disableAutoFill
|
||||
import io.legato.kazusa.utils.hideSoftInput
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import io.legato.kazusa.utils.windowSize
|
||||
|
||||
|
||||
abstract class BaseActivity<VB : ViewBinding>(
|
||||
val fullScreen: Boolean = true,
|
||||
private val theme: Theme = Theme.Auto,
|
||||
private val toolBarTheme: Theme = Theme.Auto,
|
||||
private val transparent: Boolean = false,
|
||||
private val imageBg: Boolean = true
|
||||
) : AppCompatActivity() {
|
||||
|
||||
protected abstract val binding: VB
|
||||
|
||||
val isInMultiWindow: Boolean
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
get() {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
isInMultiWindowMode
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
super.attachBaseContext(AppContextWrapper.wrap(newBase))
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
parent: View?,
|
||||
name: String,
|
||||
context: Context,
|
||||
attrs: AttributeSet
|
||||
): View? {
|
||||
if (AppConst.menuViewNames.contains(name) && parent?.parent is FrameLayout) {
|
||||
//(parent.parent as View).setBackgroundColor(backgroundColor)
|
||||
}
|
||||
return super.onCreateView(parent, name, context, attrs)
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
window.decorView.disableAutoFill()
|
||||
//initTheme()
|
||||
super.onCreate(savedInstanceState)
|
||||
//setupSystemBar()
|
||||
setContentView(binding.root)
|
||||
upBackgroundImage()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
findViewById<AppBarLayout>(R.id.title_bar)
|
||||
//?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen)
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
finish()
|
||||
}
|
||||
observeLiveBus()
|
||||
onActivityCreated(savedInstanceState)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration) {
|
||||
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig)
|
||||
// findViewById<TitleBar>(R.id.title_bar)
|
||||
// ?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen)
|
||||
//setupSystemBar()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
//findViewById<TitleBar>(R.id.title_bar)
|
||||
//?.onMultiWindowModeChanged(isInMultiWindow, fullScreen)
|
||||
//setupSystemBar()
|
||||
}
|
||||
|
||||
abstract fun onActivityCreated(savedInstanceState: Bundle?)
|
||||
|
||||
final override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val bool = onCompatCreateOptionsMenu(menu)
|
||||
menu.applyTint(this, toolBarTheme)
|
||||
return bool
|
||||
}
|
||||
|
||||
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
|
||||
menu.applyOpenTint(this)
|
||||
return super.onMenuOpened(featureId, menu)
|
||||
}
|
||||
|
||||
open fun onCompatCreateOptionsMenu(menu: Menu) = super.onCreateOptionsMenu(menu)
|
||||
|
||||
final override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
supportFinishAfterTransition()
|
||||
return true
|
||||
}
|
||||
return onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
open fun onCompatOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item)
|
||||
|
||||
open fun initTheme() {
|
||||
when (theme) {
|
||||
Theme.Transparent -> setTheme(R.style.AppTheme_Transparent)
|
||||
Theme.Dark -> {
|
||||
setTheme(R.style.AppTheme_Dark)
|
||||
//window.decorView.applyBackgroundTint(backgroundColor)
|
||||
}
|
||||
|
||||
Theme.Light -> {
|
||||
setTheme(R.style.AppTheme_Light)
|
||||
//window.decorView.applyBackgroundTint(backgroundColor)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// if (ColorUtils.isColorLight(primaryColor)) {
|
||||
// setTheme(R.style.AppTheme_Light)
|
||||
// } else {
|
||||
// setTheme(R.style.AppTheme_Dark)
|
||||
// }
|
||||
//window.decorView.applyBackgroundTint(backgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun upBackgroundImage() {
|
||||
if (imageBg) {
|
||||
try {
|
||||
ThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
|
||||
window.decorView.background = BitmapDrawable(resources, it)
|
||||
}
|
||||
} catch (e: OutOfMemoryError) {
|
||||
toastOnUi("背景图片太大,内存溢出")
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("加载背景出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// open fun setupSystemBar() {
|
||||
// if (fullScreen && !isInMultiWindow) {
|
||||
// fullScreen()
|
||||
// }
|
||||
// //val isTransparentStatusBar = AppConfig.isTransparentStatusBar
|
||||
// //val statusBarColor = ThemeStore.statusBarColor(this, isTransparentStatusBar)
|
||||
// //setStatusBarColorAuto(statusBarColor, isTransparentStatusBar, fullScreen)
|
||||
//// if (toolBarTheme == Theme.Dark) {
|
||||
//// setLightStatusBar(false)
|
||||
//// } else if (toolBarTheme == Theme.Light) {
|
||||
//// setLightStatusBar(true)
|
||||
//// }
|
||||
// upNavigationBarColor()
|
||||
// }
|
||||
|
||||
open fun upNavigationBarColor() {
|
||||
// if (AppConfig.immNavigationBar) {
|
||||
// setNavigationBarColorAuto(ThemeStore.navigationBarColor(this))
|
||||
// } else {
|
||||
// val nbColor = ColorUtils.darkenColor(ThemeStore.navigationBarColor(this))
|
||||
// setNavigationBarColorAuto(nbColor)
|
||||
// }
|
||||
}
|
||||
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
return try {
|
||||
super.dispatchTouchEvent(ev)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
currentFocus?.hideSoftInput()
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.DialogInterface.OnDismissListener
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BaseBottomSheetDialogFragment(
|
||||
@LayoutRes layoutID: Int
|
||||
) : BottomSheetDialogFragment(layoutID) {
|
||||
|
||||
private var onDismissListener: OnDismissListener? = null
|
||||
|
||||
fun setOnDismissListener(onDismissListener: OnDismissListener?) {
|
||||
this.onDismissListener = onDismissListener
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) {
|
||||
// setStyle(STYLE_NO_TITLE, 0)
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
onFragmentCreated(view, savedInstanceState)
|
||||
observeLiveBus()
|
||||
}
|
||||
|
||||
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
|
||||
|
||||
override fun show(manager: FragmentManager, tag: String?) {
|
||||
kotlin.runCatching {
|
||||
manager.beginTransaction().remove(this).commit()
|
||||
super.show(manager, tag)
|
||||
}.onFailure {
|
||||
AppLog.put("显示BottomSheetDialog失败 tag:$tag\n$it")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
onDismissListener?.onDismiss(dialog)
|
||||
}
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = lifecycleScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
) = Coroutine.async(scope, context) { block() }
|
||||
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.DialogInterface.OnDismissListener
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import io.legato.kazusa.lib.theme.ThemeStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
abstract class BaseDialogFragment(
|
||||
@LayoutRes layoutID: Int,
|
||||
private val adaptationSoftKeyboard: Boolean = false
|
||||
) : DialogFragment(layoutID) {
|
||||
|
||||
private var onDismissListener: OnDismissListener? = null
|
||||
|
||||
fun setOnDismissListener(onDismissListener: OnDismissListener?) {
|
||||
this.onDismissListener = onDismissListener
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
// if (adaptationSoftKeyboard) {
|
||||
// dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
|
||||
// } else if (AppConfig.isEInkMode) {
|
||||
// dialog?.window?.let {
|
||||
// it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
// val attr = it.attributes
|
||||
// attr.dimAmount = 0.0f
|
||||
// attr.windowAnimations = 0
|
||||
// it.attributes = attr
|
||||
// it.decorView.setBackgroundKeepPadding(R.color.transparent)
|
||||
// }
|
||||
// // 修改gravity的时机一般在子类的onStart方法中, 因此需要在onStart之后执行.
|
||||
// lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
||||
// if (event == Lifecycle.Event.ON_START) {
|
||||
// when (dialog?.window?.attributes?.gravity) {
|
||||
// Gravity.TOP -> view?.setBackgroundResource(R.drawable.bg_eink_border_bottom)
|
||||
// Gravity.BOTTOM -> view?.setBackgroundResource(R.drawable.bg_eink_border_top)
|
||||
// else -> {
|
||||
// val padding = 2.dpToPx();
|
||||
// view?.setPadding(padding, padding, padding, padding)
|
||||
// view?.setBackgroundResource(R.drawable.bg_eink_border_dialog)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (adaptationSoftKeyboard) {
|
||||
view.findViewById<View>(R.id.vw_bg)?.setOnClickListener(null)
|
||||
view.setOnClickListener { dismiss() }
|
||||
} else if (!AppConfig.isEInkMode) {
|
||||
view.setBackgroundColor(ThemeStore.backgroundColor())
|
||||
}
|
||||
onFragmentCreated(view, savedInstanceState)
|
||||
observeLiveBus()
|
||||
}
|
||||
|
||||
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
|
||||
|
||||
override fun show(manager: FragmentManager, tag: String?) {
|
||||
kotlin.runCatching {
|
||||
//在每个add事务前增加一个remove事务,防止连续的add
|
||||
manager.beginTransaction().remove(this).commit()
|
||||
super.show(manager, tag)
|
||||
}.onFailure {
|
||||
AppLog.put("显示对话框失败 tag:$tag", it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
onDismissListener?.onDismiss(dialog)
|
||||
}
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = lifecycleScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
) = Coroutine.async(scope, context) { block() }
|
||||
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.view.SupportMenuInflater
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.legato.kazusa.utils.applyTint
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class BaseFragment(@LayoutRes layoutID: Int) : Fragment(layoutID) {
|
||||
|
||||
var supportToolbar: Toolbar? = null
|
||||
private set
|
||||
|
||||
val menuInflater: MenuInflater
|
||||
@SuppressLint("RestrictedApi")
|
||||
get() = SupportMenuInflater(requireContext())
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onMultiWindowModeChanged()
|
||||
observeLiveBus()
|
||||
onFragmentCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
|
||||
|
||||
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) {
|
||||
super.onMultiWindowModeChanged(isInMultiWindowMode)
|
||||
onMultiWindowModeChanged()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
onMultiWindowModeChanged()
|
||||
}
|
||||
|
||||
private fun onMultiWindowModeChanged() {
|
||||
// (activity as? BaseActivity<*>)?.let {
|
||||
// view?.findViewById<TitleBar>(R.id.title_bar)
|
||||
// ?.onMultiWindowModeChanged(it.isInMultiWindow, it.fullScreen)
|
||||
// }
|
||||
}
|
||||
|
||||
fun setSupportToolbar(toolbar: Toolbar) {
|
||||
supportToolbar = toolbar
|
||||
supportToolbar?.let {
|
||||
it.menu.apply {
|
||||
onCompatCreateOptionsMenu(this)
|
||||
applyTint(requireContext())
|
||||
}
|
||||
|
||||
it.setOnMenuItemClickListener { item ->
|
||||
onCompatOptionsItemSelected(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
|
||||
open fun onCompatCreateOptionsMenu(menu: Menu) {
|
||||
}
|
||||
|
||||
open fun onCompatOptionsItemSelected(item: MenuItem) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
|
||||
|
||||
abstract class BasePrefDialogFragment(
|
||||
) : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (AppConfig.isEInkMode) {
|
||||
// dialog?.window?.let {
|
||||
// it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
// val attr = it.attributes
|
||||
// attr.dimAmount = 0.0f
|
||||
// attr.windowAnimations = 0
|
||||
// it.attributes = attr
|
||||
// it.setBackgroundDrawableResource(R.color.transparent)
|
||||
// }
|
||||
|
||||
// 修改gravity的时机一般在子类的onStart方法中, 因此需要在onStart之后执行.
|
||||
lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
||||
// if (event == Lifecycle.Event.ON_START) {
|
||||
// when (dialog?.window?.attributes?.gravity) {
|
||||
// Gravity.TOP -> view?.setBackgroundResource(R.drawable.bg_eink_border_bottom)
|
||||
// Gravity.BOTTOM -> view?.setBackgroundResource(R.drawable.bg_eink_border_top)
|
||||
// else -> {
|
||||
// val padding = 2.dpToPx();
|
||||
// view?.setPadding(padding, padding, padding, padding)
|
||||
// view?.setBackgroundResource(R.drawable.bg_eink_border_dialog)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.help.LifecycleHelp
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import io.legato.kazusa.lib.permission.Permissions
|
||||
import io.legato.kazusa.lib.permission.PermissionsCompat
|
||||
import io.legato.kazusa.utils.LogUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BaseService : LifecycleService() {
|
||||
|
||||
private val simpleName = this::class.simpleName.toString()
|
||||
private var isForeground = false
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = lifecycleScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
executeContext: CoroutineContext = Dispatchers.Main,
|
||||
semaphore: Semaphore? = null,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
) = Coroutine.async(scope, context, start, executeContext, semaphore, block)
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
LifecycleHelp.onServiceCreate(this)
|
||||
checkPermission()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
LogUtils.d(simpleName) {
|
||||
"onStartCommand $intent ${intent?.toUri(0)}"
|
||||
}
|
||||
if (!isForeground) {
|
||||
startForegroundNotification()
|
||||
isForeground = true
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
LogUtils.d(simpleName, "onTaskRemoved")
|
||||
super.onTaskRemoved(rootIntent)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
return null
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
LifecycleHelp.onServiceDestroy(this)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onTimeout(startId: Int) {
|
||||
super.onTimeout(startId)
|
||||
LogUtils.d(simpleName, "onTimeout startId:$startId")
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启前台服务并发送通知
|
||||
*/
|
||||
open fun startForegroundNotification() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测通知权限和后台权限
|
||||
*/
|
||||
private fun checkPermission() {
|
||||
PermissionsCompat.Builder()
|
||||
.addPermissions(Permissions.POST_NOTIFICATIONS)
|
||||
.rationale(R.string.notification_permission_rationale)
|
||||
.onGranted {
|
||||
if (lifecycleScope.isActive) {
|
||||
startForegroundNotification()
|
||||
}
|
||||
}
|
||||
.request()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PermissionsCompat.Builder()
|
||||
.addPermissions(Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
||||
.rationale(R.string.ignore_battery_permission_rationale)
|
||||
.request()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legato.kazusa.App
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@Suppress("unused")
|
||||
open class BaseViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
val context: Context by lazy { this.getApplication<App>() }
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
executeContext: CoroutineContext = Dispatchers.Main,
|
||||
semaphore: Semaphore? = null,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): Coroutine<T> {
|
||||
return Coroutine.async(scope, context, start, executeContext, semaphore, block)
|
||||
}
|
||||
|
||||
fun <T> executeLazy(
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
executeContext: CoroutineContext = Dispatchers.Main,
|
||||
semaphore: Semaphore? = null,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): Coroutine<T> {
|
||||
return Coroutine.async(
|
||||
scope, context, CoroutineStart.LAZY, executeContext, semaphore, block
|
||||
)
|
||||
}
|
||||
|
||||
fun <R> submit(
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
block: suspend CoroutineScope.() -> Deferred<R>
|
||||
): Coroutine<R> {
|
||||
return Coroutine.async(scope, context) { block().await() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# 基类
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import io.legato.kazusa.constant.Theme
|
||||
|
||||
abstract class VMBaseActivity<VB : ViewBinding, VM : ViewModel>(
|
||||
fullScreen: Boolean = true,
|
||||
theme: Theme = Theme.Auto,
|
||||
toolBarTheme: Theme = Theme.Auto,
|
||||
transparent: Boolean = false,
|
||||
imageBg: Boolean = true
|
||||
) : BaseActivity<VB>(fullScreen, theme, toolBarTheme, transparent, imageBg) {
|
||||
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legato.kazusa.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
abstract class VMBaseFragment<VM : ViewModel>(layoutID: Int) : BaseFragment(layoutID) {
|
||||
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package io.legato.kazusa.base.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import splitties.views.onLongClick
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/12/15.
|
||||
*/
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Context) :
|
||||
RecyclerView.Adapter<ItemViewHolder>() {
|
||||
|
||||
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private val asyncListDiffer: AsyncListDiffer<ITEM> by lazy {
|
||||
AsyncListDiffer(this, diffItemCallback).apply {
|
||||
addListListener { _, _ ->
|
||||
onCurrentListChanged()
|
||||
if (keepScrollPosition) {
|
||||
layoutManager?.onRestoreInstanceState(layoutState)
|
||||
layoutState = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
|
||||
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
|
||||
|
||||
private var layoutManager: RecyclerView.LayoutManager? = null
|
||||
private var layoutState: Parcelable? = null
|
||||
|
||||
var itemAnimation: ItemAnimation? = null
|
||||
|
||||
abstract val diffItemCallback: DiffUtil.ItemCallback<ITEM>
|
||||
|
||||
open val keepScrollPosition = false
|
||||
|
||||
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {
|
||||
itemLongClickListener = listener
|
||||
}
|
||||
|
||||
fun bindToRecyclerView(recyclerView: RecyclerView) {
|
||||
recyclerView.adapter = this
|
||||
}
|
||||
|
||||
fun setItems(items: List<ITEM>?) {
|
||||
kotlin.runCatching {
|
||||
if (keepScrollPosition) {
|
||||
layoutState = layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
asyncListDiffer.submitList(items?.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
fun setItem(position: Int, item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
asyncListDiffer.currentList[position] = item
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val index = asyncListDiffer.currentList.indexOf(item)
|
||||
if (index >= 0) {
|
||||
asyncListDiffer.currentList[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateItem(position: Int, payload: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = itemCount
|
||||
if (position in 0 until size) {
|
||||
notifyItemChanged(position, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = itemCount
|
||||
if (fromPosition in 0 until size && toPosition in 0 until size) {
|
||||
notifyItemRangeChanged(
|
||||
fromPosition,
|
||||
toPosition - fromPosition + 1,
|
||||
payloads
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty() = asyncListDiffer.currentList.isEmpty()
|
||||
|
||||
fun isNotEmpty() = asyncListDiffer.currentList.isNotEmpty()
|
||||
|
||||
fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position)
|
||||
|
||||
fun getItems(): List<ITEM> = asyncListDiffer.currentList
|
||||
|
||||
/**
|
||||
* grid 模式下使用
|
||||
*/
|
||||
protected open fun getSpanSize(viewType: Int, position: Int) = 1
|
||||
|
||||
final override fun getItemCount() = getItems().size
|
||||
|
||||
final override fun getItemViewType(position: Int): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
|
||||
return ItemViewHolder(getViewBinding(parent))
|
||||
}
|
||||
|
||||
protected abstract fun getViewBinding(parent: ViewGroup): VB
|
||||
|
||||
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {}
|
||||
|
||||
open fun onCurrentListChanged() {
|
||||
//可继承
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
final override fun onBindViewHolder(
|
||||
holder: ItemViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
registerListener(holder, (holder.binding as VB))
|
||||
registerItemListener(holder)
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
convert(holder, holder.binding, it, payloads)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerItemListener(holder: ItemViewHolder) {
|
||||
if (itemClickListener != null) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
itemClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemLongClickListener != null) {
|
||||
holder.itemView.onLongClick {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
itemLongClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: ItemViewHolder) {
|
||||
super.onViewAttachedToWindow(holder)
|
||||
addAnimation(holder)
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
val manager = recyclerView.layoutManager
|
||||
layoutManager = manager
|
||||
if (manager is GridLayoutManager) {
|
||||
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return getSpanSize(getItemViewType(position), position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAnimation(holder: ItemViewHolder) {
|
||||
itemAnimation?.let {
|
||||
if (it.itemAnimEnabled) {
|
||||
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {
|
||||
startAnimation(holder, it)
|
||||
it.itemAnimStartPosition = holder.layoutPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {
|
||||
item.itemAnimation?.let {
|
||||
for (anim in it.getAnimators(holder.itemView)) {
|
||||
anim.setDuration(item.itemAnimDuration).start()
|
||||
anim.interpolator = item.itemAnimInterpolator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,
|
||||
* 使用getItem(holder.layoutPosition)来获取item
|
||||
*/
|
||||
abstract fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: VB,
|
||||
item: ITEM,
|
||||
payloads: MutableList<Any>
|
||||
)
|
||||
|
||||
/**
|
||||
* 注册事件
|
||||
*/
|
||||
abstract fun registerListener(holder: ItemViewHolder, binding: VB)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.legato.kazusa.base.adapter
|
||||
|
||||
import android.view.animation.Interpolator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import io.legato.kazusa.base.adapter.animations.*
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/12/15.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class ItemAnimation private constructor() {
|
||||
|
||||
var itemAnimEnabled = false
|
||||
var itemAnimFirstOnly = true
|
||||
var itemAnimation: BaseAnimation? = null
|
||||
var itemAnimInterpolator: Interpolator = LinearInterpolator()
|
||||
var itemAnimDuration: Long = 300L
|
||||
var itemAnimStartPosition: Int = -1
|
||||
|
||||
fun interpolator(interpolator: Interpolator) = apply {
|
||||
itemAnimInterpolator = interpolator
|
||||
}
|
||||
|
||||
fun duration(duration: Long) = apply {
|
||||
itemAnimDuration = duration
|
||||
}
|
||||
|
||||
fun startPosition(startPos: Int) = apply {
|
||||
itemAnimStartPosition = startPos
|
||||
}
|
||||
|
||||
fun animation(animationType: Int = NONE, animation: BaseAnimation? = null) = apply {
|
||||
if (animation != null) {
|
||||
itemAnimation = animation
|
||||
} else {
|
||||
when (animationType) {
|
||||
FADE_IN -> itemAnimation = AlphaInAnimation()
|
||||
SCALE_IN -> itemAnimation = ScaleInAnimation()
|
||||
BOTTOM_SLIDE_IN -> itemAnimation = SlideInBottomAnimation()
|
||||
LEFT_SLIDE_IN -> itemAnimation = SlideInLeftAnimation()
|
||||
RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enabled(enabled: Boolean) = apply {
|
||||
itemAnimEnabled = enabled
|
||||
}
|
||||
|
||||
fun firstOnly(firstOnly: Boolean) = apply {
|
||||
itemAnimFirstOnly = firstOnly
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NONE: Int = 0x00000000
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val FADE_IN: Int = 0x00000001
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val SCALE_IN: Int = 0x00000002
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val BOTTOM_SLIDE_IN: Int = 0x00000003
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val LEFT_SLIDE_IN: Int = 0x00000004
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val RIGHT_SLIDE_IN: Int = 0x00000005
|
||||
|
||||
fun create() = ItemAnimation()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.legato.kazusa.base.adapter
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/11/28.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class ItemViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
@@ -0,0 +1,486 @@
|
||||
package io.legato.kazusa.base.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import io.legato.kazusa.utils.buildMainHandler
|
||||
import io.legato.kazusa.utils.withTimeoutOrNullAsync
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import splitties.views.onLongClick
|
||||
import java.util.Collections
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/11/24.
|
||||
*
|
||||
* 通用的adapter 可添加header,footer,以及不同类型item
|
||||
*/
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Context) :
|
||||
RecyclerView.Adapter<ItemViewHolder>() {
|
||||
|
||||
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private val headerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() }
|
||||
private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() }
|
||||
|
||||
private val items: MutableList<ITEM> = mutableListOf()
|
||||
|
||||
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
|
||||
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
|
||||
|
||||
private var diffJob: Coroutine<*>? = null
|
||||
|
||||
var itemAnimation: ItemAnimation? = null
|
||||
|
||||
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {
|
||||
itemLongClickListener = listener
|
||||
}
|
||||
|
||||
fun bindToRecyclerView(recyclerView: RecyclerView) {
|
||||
recyclerView.adapter = this
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = headerItems.size()
|
||||
headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = getActualItemCount() + footerItems.size()
|
||||
footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = headerItems.indexOfValue(header)
|
||||
if (index >= 0) {
|
||||
headerItems.remove(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = footerItems.indexOfValue(footer)
|
||||
if (index >= 0) {
|
||||
footerItems.remove(index)
|
||||
notifyItemRemoved(getActualItemCount() + index - 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun setItems(items: List<ITEM>?) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.isNotEmpty()) {
|
||||
this.items.clear()
|
||||
}
|
||||
if (items != null) {
|
||||
this.items.addAll(items)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setItems(
|
||||
items: List<ITEM>?,
|
||||
itemCallback: DiffUtil.ItemCallback<ITEM>,
|
||||
skipDiff: Boolean = false
|
||||
) {
|
||||
kotlin.runCatching {
|
||||
val oldItems = this.items.toList()
|
||||
val itemsSize = items?.size ?: 0
|
||||
val headerCount = getHeaderCount()
|
||||
val footerCount = getFooterCount()
|
||||
val callback = object : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int {
|
||||
return itemCount
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return itemsSize + headerCount + footerCount
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldItems.getOrNull(oldItemPosition - headerCount)
|
||||
?: return true
|
||||
val newItem = items?.getOrNull(newItemPosition - headerCount)
|
||||
?: return true
|
||||
return itemCallback.areItemsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
): Boolean {
|
||||
val oldItem = oldItems.getOrNull(oldItemPosition - headerCount)
|
||||
?: return true
|
||||
val newItem = items?.getOrNull(newItemPosition - headerCount)
|
||||
?: return true
|
||||
return itemCallback.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
||||
val oldItem = oldItems.getOrNull(oldItemPosition - headerCount)
|
||||
?: return null
|
||||
val newItem = items?.getOrNull(newItemPosition - headerCount)
|
||||
?: return null
|
||||
return itemCallback.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
diffJob?.cancel()
|
||||
diffJob = Coroutine.async {
|
||||
val diffResult = if (skipDiff) withTimeoutOrNullAsync(500L) {
|
||||
DiffUtil.calculateDiff(callback, itemsSize < 2000)
|
||||
} else {
|
||||
DiffUtil.calculateDiff(callback, itemsSize < 2000)
|
||||
}
|
||||
ensureActive()
|
||||
handler.post {
|
||||
if (diffResult == null) {
|
||||
setItems(items)
|
||||
return@post
|
||||
}
|
||||
if (this@RecyclerAdapter.items.isNotEmpty()) {
|
||||
this@RecyclerAdapter.items.clear()
|
||||
}
|
||||
if (items != null) {
|
||||
this@RecyclerAdapter.items.addAll(items)
|
||||
}
|
||||
diffResult.dispatchUpdatesTo(this@RecyclerAdapter)
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setItem(position: Int, item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val oldSize = getActualItemCount()
|
||||
if (position in 0 until oldSize) {
|
||||
this.items[position] = item
|
||||
notifyItemChanged(position + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val oldSize = getActualItemCount()
|
||||
if (this.items.add(item)) {
|
||||
notifyItemInserted(oldSize + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addItems(position: Int, newItems: List<ITEM>) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.addAll(position, newItems)) {
|
||||
notifyItemRangeInserted(position + getHeaderCount(), newItems.size)
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun addItems(newItems: List<ITEM>) {
|
||||
kotlin.runCatching {
|
||||
val oldSize = getActualItemCount()
|
||||
if (this.items.addAll(newItems)) {
|
||||
if (oldSize == 0 && getHeaderCount() == 0) {
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size)
|
||||
}
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeItem(position: Int) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.removeAt(position) != null) {
|
||||
notifyItemRemoved(position + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.remove(item)) {
|
||||
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun removeItems(items: List<ITEM>) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.removeAll(items)) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun swapItem(oldPosition: Int, newPosition: Int) {
|
||||
kotlin.runCatching {
|
||||
val size = getActualItemCount()
|
||||
if (oldPosition in 0 until size && newPosition in 0 until size) {
|
||||
val srcPosition = oldPosition + getHeaderCount()
|
||||
val targetPosition = newPosition + getHeaderCount()
|
||||
Collections.swap(this.items, srcPosition, targetPosition)
|
||||
notifyItemMoved(srcPosition, targetPosition)
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val index = this.items.indexOf(item)
|
||||
if (index >= 0) {
|
||||
this.items[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItem(position: Int, payload: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = getActualItemCount()
|
||||
if (position in 0 until size) {
|
||||
notifyItemChanged(position + getHeaderCount(), payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = getActualItemCount()
|
||||
if (fromPosition in 0 until size && toPosition in 0 until size) {
|
||||
notifyItemRangeChanged(
|
||||
fromPosition + getHeaderCount(),
|
||||
toPosition - fromPosition + 1,
|
||||
payloads
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun clearItems() {
|
||||
kotlin.runCatching {
|
||||
this.items.clear()
|
||||
notifyDataSetChanged()
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty() = items.isEmpty()
|
||||
|
||||
fun isNotEmpty() = items.isNotEmpty()
|
||||
|
||||
/**
|
||||
* 除去header和footer
|
||||
*/
|
||||
fun getActualItemCount() = items.size
|
||||
|
||||
|
||||
fun getHeaderCount() = headerItems.size()
|
||||
|
||||
|
||||
fun getFooterCount() = footerItems.size()
|
||||
|
||||
fun getItem(position: Int): ITEM? = items.getOrNull(position)
|
||||
|
||||
fun getItemByLayoutPosition(position: Int) = items.getOrNull(getActualPosition(position))
|
||||
|
||||
fun getItems(): List<ITEM> = items.toList()
|
||||
|
||||
protected open fun getItemViewType(item: ITEM, position: Int) = 0
|
||||
|
||||
/**
|
||||
* grid 模式下使用
|
||||
*/
|
||||
protected open fun getSpanSize(viewType: Int, position: Int) = 1
|
||||
|
||||
final override fun getItemCount() = getActualItemCount() + getHeaderCount() + getFooterCount()
|
||||
|
||||
final override fun getItemViewType(position: Int) = when {
|
||||
isHeader(position) -> TYPE_HEADER_VIEW + position
|
||||
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()
|
||||
else -> getItemByLayoutPosition(position)?.let {
|
||||
getItemViewType(it, getActualPosition(position))
|
||||
} ?: 0
|
||||
}
|
||||
|
||||
open fun onCurrentListChanged() {
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when {
|
||||
viewType < TYPE_HEADER_VIEW + getHeaderCount() -> {
|
||||
ItemViewHolder(headerItems.get(viewType).invoke(parent))
|
||||
}
|
||||
|
||||
viewType >= TYPE_FOOTER_VIEW -> {
|
||||
ItemViewHolder(footerItems.get(viewType).invoke(parent))
|
||||
}
|
||||
|
||||
else -> {
|
||||
ItemViewHolder(getViewBinding(parent))
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun getViewBinding(parent: ViewGroup): VB
|
||||
|
||||
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
final override fun onBindViewHolder(
|
||||
holder: ItemViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
|
||||
registerListener(holder, (holder.binding as VB))
|
||||
registerItemListener(holder)
|
||||
getItemByLayoutPosition(holder.layoutPosition)?.let { item ->
|
||||
convert(holder, holder.binding, item, payloads)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerItemListener(holder: ItemViewHolder) {
|
||||
if (itemClickListener != null) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItemByLayoutPosition(holder.layoutPosition)?.let {
|
||||
itemClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemLongClickListener != null) {
|
||||
holder.itemView.onLongClick {
|
||||
getItemByLayoutPosition(holder.layoutPosition)?.let {
|
||||
itemLongClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: ItemViewHolder) {
|
||||
super.onViewAttachedToWindow(holder)
|
||||
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
|
||||
addAnimation(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
val manager = recyclerView.layoutManager
|
||||
if (manager is GridLayoutManager) {
|
||||
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return getSpanSize(getItemViewType(position), position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isHeader(position: Int) = position < getHeaderCount()
|
||||
|
||||
private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount()
|
||||
|
||||
private fun getActualPosition(position: Int) = position - getHeaderCount()
|
||||
|
||||
private fun addAnimation(holder: ItemViewHolder) {
|
||||
itemAnimation?.let {
|
||||
if (it.itemAnimEnabled) {
|
||||
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {
|
||||
startAnimation(holder, it)
|
||||
it.itemAnimStartPosition = holder.layoutPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {
|
||||
item.itemAnimation?.let {
|
||||
for (anim in it.getAnimators(holder.itemView)) {
|
||||
anim.setDuration(item.itemAnimDuration).start()
|
||||
anim.interpolator = item.itemAnimInterpolator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,
|
||||
* 使用getItem(holder.layoutPosition)来获取item
|
||||
*/
|
||||
abstract fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: VB,
|
||||
item: ITEM,
|
||||
payloads: MutableList<Any>
|
||||
)
|
||||
|
||||
/**
|
||||
* 注册事件
|
||||
*/
|
||||
abstract fun registerListener(holder: ItemViewHolder, binding: VB)
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER_VIEW = Int.MIN_VALUE
|
||||
const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999
|
||||
private val handler by lazy { buildMainHandler() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.legato.kazusa.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) :
|
||||
BaseAnimation {
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f))
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_ALPHA_FROM = 0f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legato.kazusa.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* adapter item 动画
|
||||
*/
|
||||
interface BaseAnimation {
|
||||
|
||||
fun getAnimators(view: View): Array<Animator>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.legato.kazusa.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) :
|
||||
BaseAnimation {
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> {
|
||||
val scaleX = ObjectAnimator.ofFloat(view, "scaleX", mFrom, 1f)
|
||||
val scaleY = ObjectAnimator.ofFloat(view, "scaleY", mFrom, 1f)
|
||||
return arrayOf(scaleX, scaleY)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_SCALE_FROM = .5f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.legato.kazusa.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
class SlideInBottomAnimation : BaseAnimation {
|
||||
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legato.kazusa.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class SlideInLeftAnimation : BaseAnimation {
|
||||
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legato.kazusa.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class SlideInRightAnimation : BaseAnimation {
|
||||
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f))
|
||||
}
|
||||
Reference in new issue
Block a user