/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2017 Mariotaku Lee * * 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 . */ 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 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 { 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 } 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, getActionBarAlpha(preferences[themeBackgroundAlphaKey])) } theme.statusBarColor = ChameleonUtils.darkenColor(theme.colorToolbar) theme.lightStatusBarMode = Chameleon.Theme.LightStatusBarMode.AUTO 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 { return getColorFromAttribute(context, android.R.attr.textColorPrimary) } fun getTextColorSecondary(context: Context): Int { 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) } fun getCardBackgroundColor(context: Context, backgroundOption: String, themeAlpha: Int): Int { val color = getColorFromAttribute(context, R.attr.cardItemBackgroundColor) 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 } fun getColorBackground(context: Context, backgroundOption: String, alpha: Int): Int { return if (isWindowFloating(context)) { getColorBackground(context) } else if (backgroundOption == VALUE_THEME_BACKGROUND_TRANSPARENT) { ColorUtils.setAlphaComponent(getColorBackground(context), alpha) } else if (backgroundOption == VALUE_THEME_BACKGROUND_SOLID) { if (isLightTheme(context)) Color.WHITE else Color.BLACK } else { getColorBackground(context) } } fun applyWindowBackground(context: Context, window: Window, backgroundOption: String, alpha: Int) { 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 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 { 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) 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) backgroundColor = if (d is ColorDrawable) { d.color } else { 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 val contentLayout = window.findViewById(com.google.android.material.R.id.action_bar_activity_content) ?: window.findViewById(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) { val contrastForegroundColor = getColorDependent(toolbarColor) toolbar.setTitleTextColor(contrastForegroundColor) toolbar.setSubtitleTextColor(contrastForegroundColor) val popupItemColor: Int val popupTheme = toolbar.popupTheme popupItemColor = if (popupTheme != 0) { getThemeForegroundColor(context, popupTheme) } else { 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) } } fun getColorFromAttribute(context: Context, @AttrRes attr: Int, styleRes: Int = 0, def: Int = 0): Int { 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() } } 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 { 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 } }