Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/util/ThemeUtils.kt

504 lines
19 KiB
Kotlin
Raw Normal View History

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util
import android.content.Context
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
2020-01-26 08:35:15 +01:00
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.appcompat.app.TwilightManagerAccessor
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.TintTypedArray
import androidx.appcompat.widget.Toolbar
import androidx.appcompat.widget.TwidereToolbar
import android.util.TypedValue
import android.view.*
import android.widget.FrameLayout
import org.mariotaku.chameleon.Chameleon
import org.mariotaku.chameleon.ChameleonUtils
import org.mariotaku.kpreferences.get
import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.SharedPreferenceConstants.VALUE_THEME_BACKGROUND_SOLID
import org.mariotaku.twidere.constant.SharedPreferenceConstants.VALUE_THEME_BACKGROUND_TRANSPARENT
import org.mariotaku.twidere.constant.themeBackgroundAlphaKey
import org.mariotaku.twidere.constant.themeBackgroundOptionKey
import org.mariotaku.twidere.constant.themeColorKey
import org.mariotaku.twidere.graphic.ActionIconDrawable
import org.mariotaku.twidere.graphic.WindowBackgroundDrawable
import org.mariotaku.twidere.graphic.iface.DoNotWrapDrawable
import org.mariotaku.twidere.preference.ThemeBackgroundPreference.MAX_ALPHA
import org.mariotaku.twidere.preference.ThemeBackgroundPreference.MIN_ALPHA
import org.mariotaku.twidere.util.menu.TwidereMenuInfo
import org.mariotaku.twidere.util.support.ViewSupport
object ThemeUtils {
2017-04-15 10:42:50 +02:00
const val ACCENT_COLOR_THRESHOLD = 192
const val DARK_COLOR_THRESHOLD = 128
fun getUserTheme(context: Context, preferences: SharedPreferences): Chameleon.Theme {
val theme = Chameleon.Theme.from(context)
val userColor = getUserAccentColor(context, preferences)
theme.colorAccent = userColor
theme.colorPrimary = userColor
val backgroundOption = preferences[themeBackgroundOptionKey]
if (theme.isToolbarColored) {
theme.colorToolbar = theme.colorPrimary
2017-04-15 10:42:50 +02:00
} else if (backgroundOption == VALUE_THEME_BACKGROUND_SOLID) {
theme.colorToolbar = if (isLightTheme(context)) {
Color.WHITE
} else {
Color.BLACK
}
}
if (isTransparentBackground(backgroundOption)) {
theme.colorToolbar = ColorUtils.setAlphaComponent(theme.colorToolbar,
2017-04-15 10:42:50 +02:00
getActionBarAlpha(preferences[themeBackgroundAlphaKey]))
}
theme.statusBarColor = ChameleonUtils.darkenColor(theme.colorToolbar)
theme.lightStatusBarMode = Chameleon.Theme.LightStatusBarMode.AUTO
2017-04-15 10:42:50 +02:00
theme.textColorLink = getOptimalAccentColor(theme.colorAccent, theme.colorForeground)
return theme
}
@StyleRes
fun getCurrentThemeResource(context: Context, @StyleRes lightTheme: Int, @StyleRes darkTheme: Int): Int {
if (TwilightManagerAccessor.isNight(context)) return darkTheme
return lightTheme
}
fun getTextColorPrimary(context: Context): Int {
2017-04-15 10:42:50 +02:00
return getColorFromAttribute(context, android.R.attr.textColorPrimary)
}
fun getTextColorSecondary(context: Context): Int {
2017-04-15 10:42:50 +02:00
return getColorFromAttribute(context, android.R.attr.textColorSecondary)
}
fun getColorBackground(context: Context, styleRes: Int = 0): Int {
return getColorFromAttribute(context, android.R.attr.colorBackground, styleRes)
}
fun getColorForeground(context: Context, styleRes: Int = 0): Int {
return getColorFromAttribute(context, android.R.attr.colorForeground, styleRes)
}
2017-04-15 10:42:50 +02:00
fun getCardBackgroundColor(context: Context, backgroundOption: String, themeAlpha: Int): Int {
2017-04-15 10:42:50 +02:00
val color = getColorFromAttribute(context, R.attr.cardItemBackgroundColor)
2020-06-09 02:21:48 +02:00
return when {
isTransparentBackground(backgroundOption) -> {
ColorUtils.setAlphaComponent(color, themeAlpha)
}
isSolidBackground(backgroundOption) -> {
TwidereColorUtils.getContrastYIQ(color, Color.WHITE, Color.BLACK)
}
else -> {
color
}
}
}
fun isLightColor(color: Int): Boolean {
return ChameleonUtils.isColorLight(color)
}
fun getColorDependent(color: Int): Int {
return ChameleonUtils.getColorDependent(color)
}
fun isSolidBackground(option: String): Boolean {
return VALUE_THEME_BACKGROUND_SOLID == option
}
fun isWindowFloating(context: Context): Boolean {
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.windowIsFloating))
try {
return a.getBoolean(0, false)
} finally {
a.recycle()
}
}
fun isTransparentBackground(option: String): Boolean {
return VALUE_THEME_BACKGROUND_TRANSPARENT == option
}
2017-04-15 10:42:50 +02:00
fun getColorBackground(context: Context, backgroundOption: String, alpha: Int): Int {
2020-06-08 23:19:10 +02:00
return if (isWindowFloating(context)) {
getColorBackground(context)
} else if (backgroundOption == VALUE_THEME_BACKGROUND_TRANSPARENT) {
2020-06-08 23:19:10 +02:00
ColorUtils.setAlphaComponent(getColorBackground(context), alpha)
} else if (backgroundOption == VALUE_THEME_BACKGROUND_SOLID) {
2020-06-08 23:19:10 +02:00
if (isLightTheme(context)) Color.WHITE else Color.BLACK
} else {
2020-06-08 23:19:10 +02:00
getColorBackground(context)
}
}
fun applyWindowBackground(context: Context, window: Window, backgroundOption: String, alpha: Int) {
2020-06-09 02:21:48 +02:00
when {
isWindowFloating(context) -> {
window.setBackgroundDrawable(getWindowBackground(context))
}
VALUE_THEME_BACKGROUND_TRANSPARENT == backgroundOption -> {
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER)
window.setBackgroundDrawable(getWindowBackgroundFromThemeApplyAlpha(context, alpha))
}
VALUE_THEME_BACKGROUND_SOLID == backgroundOption -> {
window.setBackgroundDrawable(ColorDrawable(if (isLightTheme(context)) Color.WHITE else Color.BLACK))
}
else -> {
window.setBackgroundDrawable(getWindowBackground(context))
}
}
}
fun wrapMenuIcon(menu: Menu, itemColor: Int, subItemColor: Int, vararg excludeGroups: Int) {
for (i in 0 until menu.size()) {
val item = menu.getItem(i)
wrapMenuItemIcon(item, itemColor, *excludeGroups)
if (item.hasSubMenu()) {
wrapMenuIcon(item.subMenu, subItemColor, subItemColor, *excludeGroups)
}
}
}
fun wrapMenuIcon(view: ActionMenuView,
colorDark: Int = ContextCompat.getColor(view.context, R.color.action_icon_dark),
colorLight: Int = ContextCompat.getColor(view.context, R.color.action_icon_light),
vararg excludeGroups: Int) {
val context = view.context
2017-04-15 10:42:50 +02:00
val itemBackgroundColor = getColorBackground(context)
val popupItemBackgroundColor = getColorBackground(context, view.popupTheme)
val itemColor = TwidereColorUtils.getContrastYIQ(itemBackgroundColor, colorDark, colorLight)
val popupItemColor = TwidereColorUtils.getContrastYIQ(popupItemBackgroundColor, colorDark, colorLight)
val menu = view.menu
var k = 0
for (i in 0 until menu.size()) {
val item = menu.getItem(i)
wrapMenuItemIcon(item, itemColor, *excludeGroups)
if (item.hasSubMenu()) {
wrapMenuIcon(item.subMenu, popupItemColor, popupItemColor, *excludeGroups)
}
if (item.isVisible) {
k++
}
}
}
fun wrapToolbarMenuIcon(view: ActionMenuView, itemColor: Int, popupItemColor: Int, vararg excludeGroups: Int) {
val menu = view.menu
var k = 0
for (i in 0 until menu.size()) {
val item = menu.getItem(i)
wrapMenuItemIcon(item, itemColor, *excludeGroups)
if (item.hasSubMenu()) {
wrapMenuIcon(item.subMenu, popupItemColor, popupItemColor, *excludeGroups)
}
if (item.isVisible) {
k++
}
}
}
fun wrapMenuItemIcon(item: MenuItem, itemColor: Int, vararg excludeGroups: Int) {
if (item.groupId in excludeGroups) return
val icon = item.icon?.takeUnless { it is DoNotWrapDrawable } ?: return
if (icon is ActionIconDrawable) {
icon.defaultColor = itemColor
item.icon = icon
return
}
icon.mutate()
val callback = icon.callback
val newIcon = ActionIconDrawable(icon, itemColor)
newIcon.callback = callback
item.icon = newIcon
}
fun getActionIconColor(context: Context): Int {
2017-04-15 10:42:50 +02:00
val itemBackgroundColor = getColorBackground(context)
return getActionIconColor(context, itemBackgroundColor)
}
fun getActionIconColor(context: Context, backgroundColor: Int): Int {
val colorDark = ContextCompat.getColor(context, R.color.action_icon_dark)
val colorLight = ContextCompat.getColor(context, R.color.action_icon_light)
2017-04-15 10:42:50 +02:00
return if (isLightColor(backgroundColor)) colorDark else colorLight
}
fun getSelectableItemBackgroundDrawable(context: Context): Drawable? {
return getDrawableFromThemeAttribute(context, android.R.attr.selectableItemBackground)
}
fun getImageHighlightDrawable(context: Context): Drawable? {
return getSelectableItemBackgroundDrawable(context)?.apply {
alpha = 0x80
}
}
fun isLightTheme(context: Context): Boolean {
val a = context.obtainStyledAttributes(intArrayOf(R.attr.isLightTheme))
try {
return a.getBoolean(0, false)
} finally {
a.recycle()
}
}
fun getWindowBackgroundFromThemeApplyAlpha(context: Context, alpha: Int): Drawable {
var backgroundColor: Int
val d = getWindowBackground(context)
2020-06-08 23:19:10 +02:00
backgroundColor = if (d is ColorDrawable) {
d.color
} else {
2020-06-08 23:19:10 +02:00
getColorBackground(context)
}
backgroundColor = ColorUtils.setAlphaComponent(backgroundColor,
alpha.coerceIn(MIN_ALPHA..MAX_ALPHA))
return WindowBackgroundDrawable(backgroundColor)
}
fun getWindowBackground(context: Context): Drawable? {
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
try {
return a.getDrawable(0)
} finally {
a.recycle()
}
}
fun getThemeForegroundColor(context: Context): Int {
return getThemeForegroundColor(context, 0)
}
fun getThemeForegroundColor(context: Context, themeRes: Int): Int {
val value = TypedValue()
val theme: Resources.Theme
if (themeRes != 0) {
theme = context.resources.newTheme()
theme.applyStyle(themeRes, false)
} else {
theme = context.theme
}
if (!theme.resolveAttribute(android.R.attr.colorForeground, value, true)) {
return 0
}
if (value.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
// windowBackground is a color
return value.data
}
return 0
}
fun getActionBarAlpha(alpha: Int): Int {
val normalizedAlpha = alpha.coerceIn(0, 0xFF)
val delta = MAX_ALPHA - normalizedAlpha
return (MAX_ALPHA - delta / 2).coerceIn(MIN_ALPHA, MAX_ALPHA)
}
fun getActionBarHeight(context: Context): Int {
val tv = TypedValue()
val theme = context.theme
val attr = R.attr.actionBarSize
if (theme.resolveAttribute(attr, tv, true)) {
return TypedValue.complexToDimensionPixelSize(tv.data, context.resources.displayMetrics)
}
return 0
}
fun getContrastColor(color: Int, darkColor: Int, lightColor: Int): Int {
if (TwidereColorUtils.getYIQLuminance(color) <= ACCENT_COLOR_THRESHOLD) {
//return light text color
return lightColor
}
//return dark text color
return darkColor
}
fun resetCheatSheet(menuView: ActionMenuView) {
val listener = View.OnLongClickListener { v ->
if ((v as ActionMenuItemView).hasText()) return@OnLongClickListener false
val menuItem = v.itemData
Utils.showMenuItemToast(v, menuItem.title, true)
return@OnLongClickListener true
}
(0 until menuView.childCount).forEach { i ->
val child = menuView.getChildAt(i) as? ActionMenuItemView ?: return@forEach
if (child.itemData.hasSubMenu()) return@forEach
child.setOnLongClickListener(listener)
}
}
fun getOptimalAccentColor(accentColor: Int, foregroundColor: Int): Int {
val yiq = IntArray(3)
TwidereColorUtils.colorToYIQ(foregroundColor, yiq)
val foregroundColorY = yiq[0]
TwidereColorUtils.colorToYIQ(accentColor, yiq)
if (foregroundColorY < DARK_COLOR_THRESHOLD && yiq[0] <= ACCENT_COLOR_THRESHOLD) {
return accentColor
} else if (foregroundColorY > ACCENT_COLOR_THRESHOLD && yiq[0] > DARK_COLOR_THRESHOLD) {
return accentColor
}
yiq[0] = yiq[0] + (foregroundColorY - yiq[0]) / 2
return TwidereColorUtils.YIQToColor(Color.alpha(accentColor), yiq)
}
fun setCompatContentViewOverlay(window: Window, overlay: Drawable?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return
2020-01-26 08:35:15 +01:00
val contentLayout = window.findViewById<FrameLayout>(com.google.android.material.R.id.action_bar_activity_content)
2017-06-19 06:11:28 +02:00
?: window.findViewById<FrameLayout>(android.R.id.content) ?: return
ViewSupport.setForeground(contentLayout, overlay)
}
fun getSupportActionBarElevation(context: Context): Float {
val a = context.obtainStyledAttributes(null, intArrayOf(R.attr.elevation), R.attr.actionBarStyle, 0)
try {
return a.getDimension(0, 0f)
} finally {
a.recycle()
}
}
fun setActionBarOverflowColor(toolbar: Toolbar, itemColor: Int) {
if (toolbar is TwidereToolbar) {
toolbar.setItemColor(itemColor)
}
val overflowIcon = toolbar.overflowIcon
if (overflowIcon != null) {
overflowIcon.setColorFilter(itemColor, PorterDuff.Mode.SRC_ATOP)
toolbar.overflowIcon = overflowIcon
}
}
fun applyColorFilterToMenuIcon(menu: Menu, @ColorInt color: Int,
@ColorInt popupColor: Int, @ColorInt highlightColor: Int, mode: PorterDuff.Mode,
vararg excludedGroups: Int) {
var i = 0
val j = menu.size()
while (i < j) {
val item = menu.getItem(i)
val icon = item.icon
val info = item.menuInfo
if (icon != null && item.groupId !in excludedGroups) {
icon.mutate()
if (info is TwidereMenuInfo) {
val stateColor = if (info.isHighlight) info.getHighlightColor(highlightColor) else color
if (stateColor != 0) {
icon.setColorFilter(stateColor, mode)
}
} else if (color != 0) {
icon.setColorFilter(color, mode)
}
}
if (item.hasSubMenu()) {
// SubMenu item is always in popup
applyColorFilterToMenuIcon(item.subMenu, popupColor, popupColor, highlightColor, mode, *excludedGroups)
}
i++
}
}
fun applyToolbarItemColor(context: Context, toolbar: Toolbar, toolbarColor: Int) {
2017-04-15 10:42:50 +02:00
val contrastForegroundColor = getColorDependent(toolbarColor)
toolbar.setTitleTextColor(contrastForegroundColor)
toolbar.setSubtitleTextColor(contrastForegroundColor)
val popupItemColor: Int
val popupTheme = toolbar.popupTheme
2020-06-08 23:19:10 +02:00
popupItemColor = if (popupTheme != 0) {
getThemeForegroundColor(context, popupTheme)
} else {
2020-06-08 23:19:10 +02:00
getThemeForegroundColor(context)
}
val navigationIcon = toolbar.navigationIcon
if (navigationIcon != null) {
navigationIcon.setColorFilter(contrastForegroundColor, PorterDuff.Mode.SRC_ATOP)
toolbar.navigationIcon = navigationIcon
}
getThemeForegroundColor(context)
setActionBarOverflowColor(toolbar, contrastForegroundColor)
wrapToolbarMenuIcon(ViewSupport.findViewByType(toolbar, ActionMenuView::class.java),
contrastForegroundColor, popupItemColor)
if (toolbar is TwidereToolbar) {
toolbar.setItemColor(contrastForegroundColor)
}
}
2017-08-01 08:42:20 +02:00
fun getColorFromAttribute(context: Context, @AttrRes attr: Int, styleRes: Int = 0, def: Int = 0): Int {
2017-04-15 10:42:50 +02:00
val a = context.obtainStyledAttributes(null, intArrayOf(attr), 0, styleRes)
try {
return a.getColor(0, def)
} finally {
a.recycle()
}
}
fun getColorStateListFromAttribute(context: Context, @AttrRes attr: Int, styleRes: Int = 0): ColorStateList? {
val a = context.obtainStyledAttributes(null, intArrayOf(attr), 0, styleRes)
try {
return a.getColorStateList(0)
} finally {
a.recycle()
}
}
2017-08-01 08:42:20 +02:00
fun getBooleanFromAttribute(context: Context, @AttrRes attr: Int, styleRes: Int = 0, def: Boolean = false): Boolean {
val a = context.obtainStyledAttributes(null, intArrayOf(attr), 0, styleRes)
try {
return a.getBoolean(0, def)
} finally {
a.recycle()
}
}
fun getDrawableFromThemeAttribute(context: Context, @AttrRes attr: Int): Drawable {
2017-04-15 10:42:50 +02:00
val a = TintTypedArray.obtainStyledAttributes(context, null, intArrayOf(attr))
try {
return a.getDrawable(0)
} finally {
a.recycle()
}
}
private fun getUserAccentColor(context: Context, preferences: SharedPreferences): Int {
val color = preferences[themeColorKey]
if (color == 0) return ContextCompat.getColor(context, R.color.branding_color)
return color
}
}