Color schemes
This commit is contained in:
@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
@@ -51,12 +52,9 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
|
||||
val isAmoled = settings.isAmoledTheme
|
||||
val isDynamic = settings.isDynamicTheme
|
||||
when {
|
||||
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
|
||||
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
|
||||
isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet)
|
||||
setTheme(settings.colorScheme.styleResId)
|
||||
if (settings.isAmoledTheme) {
|
||||
setTheme(R.style.ThemeOverlay_Kotatsu_Amoled)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
@@ -89,9 +87,8 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
} else super.onOptionsItemSelected(item)
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
|
||||
// ActivityCompat.recreate(this)
|
||||
TODO("Test error")
|
||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
ActivityCompat.recreate(this)
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.view.children
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class CheckableButtonGroup @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = materialR.attr.materialButtonToggleGroupStyle,
|
||||
) : LinearLayout(context, attrs, defStyleAttr, materialR.style.Widget_MaterialComponents_MaterialButtonToggleGroup),
|
||||
View.OnClickListener {
|
||||
|
||||
private val originalCornerData = ArrayList<CornerData>()
|
||||
|
||||
var onCheckedChangeListener: OnCheckedChangeListener? = null
|
||||
|
||||
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
||||
if (child is MaterialButton) {
|
||||
setupButton(child)
|
||||
}
|
||||
super.addView(child, index, params)
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
updateChildShapes()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
setCheckedId(v.id)
|
||||
}
|
||||
|
||||
fun setCheckedId(@IdRes viewRes: Int) {
|
||||
children.forEach {
|
||||
(it as? MaterialButton)?.isChecked = it.id == viewRes
|
||||
}
|
||||
onCheckedChangeListener?.onCheckedChanged(this, viewRes)
|
||||
}
|
||||
|
||||
private fun updateChildShapes() {
|
||||
val childCount = childCount
|
||||
val firstVisibleChildIndex = 0
|
||||
val lastVisibleChildIndex = childCount - 1
|
||||
for (i in 0 until childCount) {
|
||||
val button: MaterialButton = getChildAt(i) as? MaterialButton ?: continue
|
||||
if (button.visibility == GONE) {
|
||||
continue
|
||||
}
|
||||
val builder = button.shapeAppearanceModel.toBuilder()
|
||||
val newCornerData: CornerData? =
|
||||
getNewCornerData(i, firstVisibleChildIndex, lastVisibleChildIndex)
|
||||
updateBuilderWithCornerData(builder, newCornerData)
|
||||
button.shapeAppearanceModel = builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupButton(button: MaterialButton) {
|
||||
button.setOnClickListener(this)
|
||||
button.isElegantTextHeight = false
|
||||
// Saves original corner data
|
||||
val shapeAppearanceModel: ShapeAppearanceModel = button.shapeAppearanceModel
|
||||
originalCornerData.add(
|
||||
CornerData(
|
||||
shapeAppearanceModel.topLeftCornerSize,
|
||||
shapeAppearanceModel.bottomLeftCornerSize,
|
||||
shapeAppearanceModel.topRightCornerSize,
|
||||
shapeAppearanceModel.bottomRightCornerSize,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNewCornerData(
|
||||
index: Int,
|
||||
firstVisibleChildIndex: Int,
|
||||
lastVisibleChildIndex: Int,
|
||||
): CornerData? {
|
||||
val cornerData: CornerData = originalCornerData.get(index)
|
||||
|
||||
// If only one (visible) child exists, use its original corners
|
||||
if (firstVisibleChildIndex == lastVisibleChildIndex) {
|
||||
return cornerData
|
||||
}
|
||||
val isHorizontal = orientation == HORIZONTAL
|
||||
if (index == firstVisibleChildIndex) {
|
||||
return if (isHorizontal) cornerData.start(this) else cornerData.top()
|
||||
}
|
||||
return if (index == lastVisibleChildIndex) {
|
||||
if (isHorizontal) cornerData.end(this) else cornerData.bottom()
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun updateBuilderWithCornerData(
|
||||
shapeAppearanceModelBuilder: ShapeAppearanceModel.Builder,
|
||||
cornerData: CornerData?,
|
||||
) {
|
||||
if (cornerData == null) {
|
||||
shapeAppearanceModelBuilder.setAllCornerSizes(0f)
|
||||
return
|
||||
}
|
||||
shapeAppearanceModelBuilder
|
||||
.setTopLeftCornerSize(cornerData.topLeft)
|
||||
.setBottomLeftCornerSize(cornerData.bottomLeft)
|
||||
.setTopRightCornerSize(cornerData.topRight)
|
||||
.setBottomRightCornerSize(cornerData.bottomRight)
|
||||
}
|
||||
|
||||
fun interface OnCheckedChangeListener {
|
||||
fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.shape.AbsoluteCornerSize
|
||||
import com.google.android.material.shape.CornerSize
|
||||
|
||||
class CornerData(
|
||||
var topLeft: CornerSize,
|
||||
var bottomLeft: CornerSize,
|
||||
var topRight: CornerSize,
|
||||
var bottomRight: CornerSize,
|
||||
) {
|
||||
|
||||
fun start(view: View): CornerData {
|
||||
return if (isLayoutRtl(view)) right() else left()
|
||||
}
|
||||
|
||||
fun end(view: View): CornerData {
|
||||
return if (isLayoutRtl(view)) left() else right()
|
||||
}
|
||||
|
||||
fun left(): CornerData {
|
||||
return CornerData(topLeft, bottomLeft, noCorner, noCorner)
|
||||
}
|
||||
|
||||
fun right(): CornerData {
|
||||
return CornerData(noCorner, noCorner, topRight, bottomRight)
|
||||
}
|
||||
|
||||
fun top(): CornerData {
|
||||
return CornerData(topLeft, noCorner, topRight, noCorner)
|
||||
}
|
||||
|
||||
fun bottom(): CornerData {
|
||||
return CornerData(noCorner, bottomLeft, noCorner, bottomRight)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val noCorner: CornerSize = AbsoluteCornerSize(0f)
|
||||
|
||||
fun isLayoutRtl(view: View): Boolean {
|
||||
return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.withClip
|
||||
import com.google.android.material.drawable.DrawableUtils
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class ShapeView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val corners = FloatArray(8)
|
||||
private val outlinePath = Path()
|
||||
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.ShapeView, defStyleAttr) {
|
||||
val cornerSize = getDimension(R.styleable.ShapeView_cornerSize, 0f)
|
||||
corners[0] = getDimension(R.styleable.ShapeView_cornerSizeTopLeft, cornerSize)
|
||||
corners[1] = corners[0]
|
||||
corners[2] = getDimension(R.styleable.ShapeView_cornerSizeTopRight, cornerSize)
|
||||
corners[3] = corners[2]
|
||||
corners[4] = getDimension(R.styleable.ShapeView_cornerSizeBottomRight, cornerSize)
|
||||
corners[5] = corners[4]
|
||||
corners[6] = getDimension(R.styleable.ShapeView_cornerSizeBottomLeft, cornerSize)
|
||||
corners[7] = corners[6]
|
||||
strokePaint.color = getColor(R.styleable.ShapeView_strokeColor, Color.TRANSPARENT)
|
||||
strokePaint.strokeWidth = getDimension(R.styleable.ShapeView_strokeWidth, 0f)
|
||||
strokePaint.style = Paint.Style.STROKE
|
||||
}
|
||||
outlineProvider = OutlineProvider()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (w != oldw || h != oldh) {
|
||||
rebuildPath()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.withClip(outlinePath) {
|
||||
super.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
if (strokePaint.strokeWidth > 0f) {
|
||||
canvas.drawPath(outlinePath, strokePaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun rebuildPath() {
|
||||
outlinePath.reset()
|
||||
val w = width
|
||||
val h = height
|
||||
if (w > 0 && h > 0) {
|
||||
outlinePath.addRoundRect(0f, 0f, w.toFloat(), h.toFloat(), corners, Path.Direction.CW)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OutlineProvider : ViewOutlineProvider() {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun getOutline(view: View?, outline: Outline) {
|
||||
val corner = corners[0]
|
||||
var isRoundRect = true
|
||||
for (item in corners) {
|
||||
if (item != corner) {
|
||||
isRoundRect = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isRoundRect) {
|
||||
outline.setRoundRect(0, 0, width, height, corner)
|
||||
} else {
|
||||
DrawableUtils.setOutlineToPath(outline, outlinePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import androidx.collection.arraySetOf
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
@@ -70,8 +69,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val theme: Int
|
||||
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
|
||||
val isDynamicTheme: Boolean
|
||||
get() = DynamicColors.isDynamicColorAvailable() && prefs.getBoolean(KEY_DYNAMIC_THEME, false)
|
||||
val colorScheme: ColorScheme
|
||||
get() = prefs.getEnumValue(KEY_COLOR_THEME, ColorScheme.default)
|
||||
|
||||
val isAmoledTheme: Boolean
|
||||
get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
|
||||
@@ -312,7 +311,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
const val KEY_LIST_MODE = "list_mode_2"
|
||||
const val KEY_THEME = "theme"
|
||||
const val KEY_DYNAMIC_THEME = "dynamic_theme"
|
||||
const val KEY_COLOR_THEME = "color_theme"
|
||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||
const val KEY_DATE_FORMAT = "date_format"
|
||||
const val KEY_SOURCES_ORDER = "sources_order_2"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.StyleRes
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
enum class ColorScheme(
|
||||
@StyleRes val styleResId: Int,
|
||||
@StringRes val titleResId: Int,
|
||||
) {
|
||||
|
||||
DEFAULT(R.style.Theme_Kotatsu, R.string.system_default),
|
||||
MONET(R.style.Theme_Kotatsu_Monet, R.string.theme_name_dynamic),
|
||||
MINT(R.style.Theme_Kotatsu_Mint, R.string.theme_name_mint),
|
||||
OCTOBER(R.style.Theme_Kotatsu_October, R.string.theme_name_october),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
val default: ColorScheme
|
||||
get() = if (DynamicColors.isDynamicColorAvailable()) {
|
||||
MONET
|
||||
} else {
|
||||
DEFAULT
|
||||
}
|
||||
|
||||
fun getAvailableList(): List<ColorScheme> {
|
||||
val list = enumValues<ColorScheme>().toMutableList()
|
||||
if (!DynamicColors.isDynamicColorAvailable()) {
|
||||
list.remove(MONET)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun safeValueOf(name: String): ColorScheme? {
|
||||
return enumValues<ColorScheme>().find { it.name == name }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import androidx.core.view.postDelayed
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.TwoStatePreference
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
@@ -56,7 +55,6 @@ class AppearanceSettingsFragment :
|
||||
entryValues = ListMode.values().names()
|
||||
setDefaultValueCompat(ListMode.GRID.name)
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = DynamicColors.isDynamicColorAvailable()
|
||||
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
|
||||
entryValues = resources.getStringArray(R.array.date_formats)
|
||||
val now = Date().time
|
||||
@@ -105,10 +103,7 @@ class AppearanceSettingsFragment :
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
|
||||
AppSettings.KEY_DYNAMIC_THEME -> {
|
||||
postRestart()
|
||||
}
|
||||
|
||||
AppSettings.KEY_COLOR_THEME,
|
||||
AppSettings.KEY_THEME_AMOLED -> {
|
||||
postRestart()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.koitharu.kotatsu.settings.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.ColorScheme
|
||||
import org.koitharu.kotatsu.databinding.ItemColorSchemeBinding
|
||||
|
||||
class ThemeChooserPreference @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.themeChooserPreferenceStyle,
|
||||
defStyleRes: Int = R.style.Preference_ThemeChooser,
|
||||
) : Preference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
private val entries = ColorScheme.getAvailableList()
|
||||
private var currentValue: ColorScheme = ColorScheme.default
|
||||
private val itemClickListener = View.OnClickListener {
|
||||
val tag = it.tag as? ColorScheme ?: return@OnClickListener
|
||||
setValueInternal(tag.name, true)
|
||||
}
|
||||
|
||||
var value: String
|
||||
get() = currentValue.name
|
||||
set(value) = setValueInternal(value, notifyChanged = true)
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
val layout = holder.findViewById(R.id.linear) as? LinearLayout ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
layout.suppressLayout(true)
|
||||
}
|
||||
layout.removeAllViews()
|
||||
for (theme in entries) {
|
||||
val context = ContextThemeWrapper(context, theme.styleResId)
|
||||
val item = ItemColorSchemeBinding.inflate(LayoutInflater.from(context), layout, false)
|
||||
item.card.isChecked = theme == currentValue
|
||||
item.textViewTitle.setText(theme.titleResId)
|
||||
item.root.tag = theme
|
||||
item.card.tag = theme
|
||||
item.imageViewCheck.isVisible = theme == currentValue
|
||||
item.root.setOnClickListener(itemClickListener)
|
||||
item.card.setOnClickListener(itemClickListener)
|
||||
layout.addView(item.root)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
layout.suppressLayout(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(defaultValue: Any?) {
|
||||
value = getPersistedString(
|
||||
when (defaultValue) {
|
||||
is String -> ColorScheme.safeValueOf(defaultValue) ?: ColorScheme.default
|
||||
is ColorScheme -> defaultValue
|
||||
else -> ColorScheme.default
|
||||
}.name,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGetDefaultValue(a: TypedArray, index: Int): Any {
|
||||
return a.getInt(index, 0)
|
||||
}
|
||||
|
||||
private fun setValueInternal(enumName: String, notifyChanged: Boolean) {
|
||||
val newValue = ColorScheme.safeValueOf(enumName) ?: return
|
||||
if (newValue != currentValue) {
|
||||
currentValue = newValue
|
||||
persistString(newValue.name)
|
||||
if (notifyChanged) {
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user