diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..4bec4ea8 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt b/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt index 0c6060b1..2aba757b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt @@ -51,16 +51,20 @@ class ColorUtil @Inject constructor(private val context: Context) { @ColorInt fun getForegroundColorForBackgroundColor(@ColorInt color: Int): Int { - val hsl = FloatArray(HSL_SIZE) - ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) - - return if (hsl[INDEX_LIGHTNESS] < LIGHTNESS_DARK_THRESHOLD) { + return if (isDarkBackground(color)) { Color.WHITE } else { ContextCompat.getColor(context, R.color.grey_900) } } + fun isDarkBackground(@ColorInt color: Int): Boolean { + val hsl = FloatArray(HSL_SIZE) + ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) + + return hsl[INDEX_LIGHTNESS] < LIGHTNESS_DARK_THRESHOLD + } + fun setLightness(@ColorInt color: Int, lightness: Float): Int { require(lightness in 0.0..1.0) { "Lightness must be between 0 and 1" } val hsl = FloatArray(HSL_SIZE) @@ -71,9 +75,17 @@ class ColorUtil @Inject constructor(private val context: Context) { return ColorUtils.HSLToColor(hsl) } + fun colorToHexString(@ColorInt color: Int): String { + return String.format(null, "#%06X", HEX_WHITE and color) + } + @ColorInt private fun String?.parseColorOrFallback(fallback: () -> Int): Int { - return this?.let { Color.parseColor(this) } ?: fallback() + return if (this?.isNotBlank() == true) { + Color.parseColor(this) + } else { + fallback() + } } @ColorInt @@ -90,5 +102,6 @@ class ColorUtil @Inject constructor(private val context: Context) { private const val HSL_SIZE: Int = 3 private const val INDEX_LIGHTNESS: Int = 2 private const val LIGHTNESS_DARK_THRESHOLD: Float = 0.6f + private const val HEX_WHITE = 0xFFFFFF } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt index 9ee03e4e..7f4233f9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt @@ -21,6 +21,7 @@ package com.nextcloud.android.common.ui.theme +import androidx.annotation.ColorInt import scheme.Scheme interface MaterialSchemes { @@ -36,5 +37,6 @@ interface MaterialSchemes { companion object { fun fromServerTheme(theme: ServerTheme): MaterialSchemes = MaterialSchemesImpl(theme) + fun fromColor(@ColorInt color: Int): MaterialSchemes = MaterialSchemesImpl(color) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemesImpl.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemesImpl.kt index 85eb3280..d76ba327 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemesImpl.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemesImpl.kt @@ -21,15 +21,18 @@ package com.nextcloud.android.common.ui.theme +import androidx.annotation.ColorInt import scheme.Scheme -internal class MaterialSchemesImpl(serverTheme: ServerTheme) : +internal class MaterialSchemesImpl : MaterialSchemes { override val lightScheme: Scheme override val darkScheme: Scheme - init { - lightScheme = Scheme.light(serverTheme.primaryColor) - darkScheme = Scheme.dark(serverTheme.primaryColor) + constructor(@ColorInt primaryColor: Int) { + lightScheme = Scheme.light(primaryColor) + darkScheme = Scheme.dark(primaryColor) } + + constructor(serverTheme: ServerTheme) : this(serverTheme.primaryColor) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt index e620bb28..46a49b74 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt @@ -28,7 +28,7 @@ import android.view.View import com.nextcloud.android.common.ui.util.PlatformThemeUtil import scheme.Scheme -open class ViewThemeUtilsBase(val schemes: MaterialSchemes) { +open class ViewThemeUtilsBase(private val schemes: MaterialSchemes) { /** * Scheme for painting elements */ @@ -40,20 +40,15 @@ open class ViewThemeUtilsBase(val schemes: MaterialSchemes) { fun getScheme(context: Context): Scheme = getSchemeInternal(context) @Suppress("MemberVisibilityCanBePrivate") + // TODO cache by context hashcode protected fun getSchemeInternal(context: Context): Scheme = when { PlatformThemeUtil.isDarkMode(context) -> schemes.darkScheme else -> schemes.lightScheme } - protected fun withScheme(view: View, block: (Scheme) -> Unit) { - block(getSchemeInternal(view.context)) - } + protected fun withScheme(view: View, block: (Scheme) -> R): R = block(getSchemeInternal(view.context)) - protected fun withScheme(context: Context, block: (Scheme) -> Unit) { - block(getSchemeInternal(context)) - } + protected fun withScheme(context: Context, block: (Scheme) -> R): R = block(getSchemeInternal(context)) - protected fun withSchemeDark(block: (Scheme) -> Unit) { - block(schemes.darkScheme) - } + protected fun withSchemeDark(block: (Scheme) -> R): R = block(schemes.darkScheme) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt index 28484745..aee8690e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt @@ -23,34 +23,47 @@ package com.nextcloud.android.common.ui.theme.utils +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.PorterDuff import android.graphics.Typeface -import android.os.Build +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable import android.text.Spannable +import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.view.MenuItem import android.view.View import android.widget.Button import android.widget.CheckBox +import android.widget.CheckedTextView import android.widget.EditText import android.widget.ImageButton import android.widget.ImageView import android.widget.ProgressBar import android.widget.RadioButton import android.widget.SeekBar +import android.widget.Switch import android.widget.TextView import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.appcompat.app.ActionBarDrawerToggle import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.BlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.google.android.material.navigation.NavigationView import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.color.ColorUtil import com.nextcloud.android.common.ui.theme.MaterialSchemes import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase -import com.nextcloud.android.common.ui.util.PlatformThemeUtil +import com.nextcloud.android.common.ui.util.buildColorStateList +import scheme.Scheme import javax.inject.Inject /** @@ -60,48 +73,171 @@ import javax.inject.Inject class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, private val colorUtil: ColorUtil) : ViewThemeUtilsBase(schemes) { - fun colorViewBackground(view: View) { + @JvmOverloads + fun colorViewBackground(view: View, colorRole: ColorRole = ColorRole.SURFACE) { withScheme(view) { scheme -> - view.setBackgroundColor(scheme.surface) + view.setBackgroundColor(colorRole.select(scheme)) + } + } + + fun colorNavigationView(navigationView: NavigationView) { + withScheme(navigationView) { scheme -> + if (navigationView.itemBackground != null) { + navigationView.itemBackground!!.setTintList( + buildColorStateList( + android.R.attr.state_checked to scheme.secondaryContainer, + -android.R.attr.state_checked to Color.TRANSPARENT + ) + ) + } + navigationView.setBackgroundColor(scheme.surface) + + val colorStateList = buildColorStateList( + android.R.attr.state_checked to scheme.onSecondaryContainer, + -android.R.attr.state_checked to scheme.onSurfaceVariant + ) + + navigationView.itemTextColor = colorStateList + navigationView.itemIconTintList = colorStateList + } + } + + fun getPrimaryColorDrawable(context: Context): Drawable { + return withScheme(context) { scheme -> + ColorDrawable(scheme.primary) } } fun colorToolbarMenuIcon(context: Context, item: MenuItem) { withScheme(context) { scheme -> - item.icon.setColorFilter(scheme.onSurface, PorterDuff.Mode.SRC_ATOP) + colorMenuItemIcon(scheme.onSurface, item) + } + } + + fun colorMenuItemText(context: Context, item: MenuItem) { + withScheme(context) { scheme: Scheme -> + colorMenuItemText(scheme.onSurface, item) + } + } + + private fun colorMenuItemIcon(@ColorInt color: Int, item: MenuItem) { + val normalDrawable = item.icon + val wrapDrawable = DrawableCompat.wrap(normalDrawable) + DrawableCompat.setTint(wrapDrawable, color) + item.icon = wrapDrawable + } + + private fun colorMenuItemText(@ColorInt color: Int, item: MenuItem) { + val newItemTitle = SpannableString(item.title) + newItemTitle.setSpan( + ForegroundColorSpan(color), + 0, + newItemTitle.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + item.title = newItemTitle + } + + @JvmOverloads + fun tintDrawable(context: Context, @DrawableRes id: Int, colorRole: ColorRole = ColorRole.PRIMARY): Drawable? { + val drawable = ResourcesCompat.getDrawable(context.resources, id, null) + return drawable?.let { + tintDrawable(context, it, colorRole) + } + } + + @Deprecated( + replaceWith = ReplaceWith( + "tintDrawable(context, id, ColorRole.PRIMARY)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use tintDrawable(context, id, ColorRole.PRIMARY) instead" + ) + fun tintPrimaryDrawable(context: Context, @DrawableRes id: Int): Drawable? { + return tintDrawable(context, id, ColorRole.PRIMARY) + } + + @JvmOverloads + fun tintDrawable(context: Context, drawable: Drawable, colorRole: ColorRole = ColorRole.PRIMARY): Drawable { + return withScheme(context) { scheme: Scheme -> + colorDrawable(drawable, colorRole.select(scheme)) } } + @Deprecated( + replaceWith = ReplaceWith( + "tintDrawable(context, drawable, ColorRole.PRIMARY)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use tintDrawable(context, drawable, ColorRole.PRIMARY) instead" + ) + fun tintPrimaryDrawable(context: Context, drawable: Drawable?): Drawable? { + return drawable?.let { tintDrawable(context, it, ColorRole.PRIMARY) } + } + + @Deprecated( + replaceWith = ReplaceWith( + "tintDrawable(context, drawable, ColorRole.ON_SURFACE)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use tintDrawable(context, drawable, ColorRole.ON_SURFACE) instead" + ) + fun tintTextDrawable(context: Context, drawable: Drawable?): Drawable? { + return drawable?.let { tintDrawable(context, it, ColorRole.ON_SURFACE) } + } + + /** + * Public for edge cases. For most cases use [tintDrawable] instead + */ + fun colorDrawable(drawable: Drawable, @ColorInt color: Int): Drawable { + val wrap = DrawableCompat.wrap(drawable) + wrap.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + color, + BlendModeCompat.SRC_ATOP + ) + return wrap + } + + fun tintToolbarArrowDrawable( + context: Context, + drawerToggle: ActionBarDrawerToggle, + drawable: Drawable + ) { + withScheme(context) { scheme: Scheme -> + val wrap = DrawableCompat.wrap(drawable) + wrap.setColorFilter(scheme.onSurface, PorterDuff.Mode.SRC_ATOP) + drawerToggle.setHomeAsUpIndicator(wrap) + drawerToggle.drawerArrowDrawable.color = scheme.onSurface + } + } + + @Deprecated(message = "Use themeStatusBar(activity) instead", replaceWith = ReplaceWith("themeStatusBar(activity)")) + @Suppress("Detekt.UnusedPrivateMember") // deprecated, to be removed fun themeStatusBar(activity: Activity, view: View) { - withScheme(view) { scheme -> - applyColorToStatusBar(activity, scheme.surface) + themeStatusBar(activity) + } + + fun themeStatusBar(activity: Activity) { + withScheme(activity) { scheme -> + colorStatusBar(activity, scheme.surface) } } - private fun applyColorToStatusBar(activity: Activity, @ColorInt color: Int) { + /** + * Public for special cases, e.g. action mode. You probably want [themeStatusBar] for most cases instead. + */ + fun colorStatusBar(activity: Activity, @ColorInt color: Int) { val window = activity.window ?: return - val isLightTheme = !PlatformThemeUtil.isDarkMode(activity) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val decor = window.decorView - if (isLightTheme) { - val systemUiFlagLightStatusBar = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or - View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - } else { - View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - } - decor.systemUiVisibility = systemUiFlagLightStatusBar - } else { - decor.systemUiVisibility = 0 - } - window.statusBarColor = color - } else if (isLightTheme) { - window.statusBarColor = Color.BLACK - } + val isLightBackground = !colorUtil.isDarkBackground(color) + val decor = window.decorView + window.statusBarColor = color + window.navigationBarColor = color + WindowInsetsControllerCompat(window, decor).isAppearanceLightStatusBars = isLightBackground + WindowInsetsControllerCompat(window, decor).isAppearanceLightNavigationBars = isLightBackground } fun resetStatusBar(activity: Activity) { - applyColorToStatusBar( + colorStatusBar( activity, ResourcesCompat.getColor( activity.resources, @@ -136,25 +272,53 @@ class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, privat } } - fun themeHorizontalProgressBar(progressBar: ProgressBar?, @ColorInt color: Int) { - if (progressBar != null) { - progressBar.indeterminateDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN) - progressBar.progressDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN) + fun themeHorizontalProgressBar(progressBar: ProgressBar) { + withScheme(progressBar) { scheme -> + themeHorizontalProgressBar(progressBar, scheme.primary) } } - fun colorPrimaryTextViewElement(textView: TextView) { + fun themeHorizontalProgressBar(progressBar: ProgressBar?, @ColorInt color: Int) { + progressBar?.indeterminateDrawable?.setColorFilter(color, PorterDuff.Mode.SRC_IN) + progressBar?.progressDrawable?.setColorFilter(color, PorterDuff.Mode.SRC_IN) + } + + @JvmOverloads + fun colorTextView(textView: TextView, colorRole: ColorRole = ColorRole.PRIMARY) { withScheme(textView) { scheme -> - textView.setTextColor(scheme.primary) + textView.setTextColor(colorRole.select(scheme)) } } + @Deprecated( + replaceWith = ReplaceWith("colorTextView(textView)"), + message = "Use colorTextView(textView) instead" + ) + fun colorPrimaryTextViewElement(textView: TextView) { + colorTextView(textView, ColorRole.PRIMARY) + } + + @Deprecated( + replaceWith = ReplaceWith( + "colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER) instead" + ) + fun colorOnSecondaryContainerTextViewElement(textView: TextView) { + colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER) + } + fun colorPrimaryTextViewElementDarkMode(textView: TextView) { withSchemeDark { scheme -> textView.setTextColor(scheme.primary) } } + @Deprecated( + replaceWith = ReplaceWith("colorViewBackground(view)"), + message = "Use colorViewBackground(view) instead" + ) fun colorPrimaryView(view: View) { withScheme(view) { scheme -> view.setBackgroundColor(scheme.primary) @@ -163,8 +327,20 @@ class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, privat /** * Colors the background as element color and the foreground as text color. + * */ + @Deprecated( + replaceWith = ReplaceWith("colorImageViewBackgroundAndIcon"), + message = "Use colorImageViewBackgroundAndIcon, which has a better name, instead" + ) fun colorImageViewButton(imageView: ImageView) { + colorImageViewBackgroundAndIcon(imageView) + } + + /** + * Colors the background as element color and the foreground as text color. + */ + fun colorImageViewBackgroundAndIcon(imageView: ImageView) { withScheme(imageView) { scheme -> imageView.imageTintList = ColorStateList.valueOf(scheme.onPrimaryContainer) imageView.backgroundTintList = ColorStateList.valueOf(scheme.primaryContainer) @@ -173,23 +349,25 @@ class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, privat fun themeImageButton(imageButton: ImageButton) { withScheme(imageButton) { scheme -> - imageButton.imageTintList = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_selected), - intArrayOf(-android.R.attr.state_selected), - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) - ), - intArrayOf( - scheme.primary, - scheme.onSurfaceVariant, - scheme.onSurfaceVariant, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) + imageButton.imageTintList = buildColorStateList( + android.R.attr.state_selected to scheme.primary, + -android.R.attr.state_selected to scheme.onSurfaceVariant, + android.R.attr.state_enabled to scheme.onSurfaceVariant, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + ON_SURFACE_OPACITY_BUTTON_DISABLED ) ) } } + /** + * In most cases you'll want to use [themeImageButton] instead. + */ + fun colorImageButton(imageButton: ImageButton?, @ColorInt color: Int) { + imageButton?.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + /** * Tints the image with element color */ @@ -200,17 +378,23 @@ class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, privat } fun colorTextButtons(vararg buttons: Button) { + withScheme(buttons[0]) { scheme -> + colorTextButtons(scheme.primary, *buttons) + } + } + + /** + * In most cases you'll want to use [colorTextButtons] instead. + */ + fun colorTextButtons(@ColorInt color: Int, vararg buttons: Button) { withScheme(buttons[0]) { scheme -> for (button in buttons) { button.setTextColor( - ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) - ), - intArrayOf( - scheme.primary, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) + buildColorStateList( + android.R.attr.state_enabled to color, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + ON_SURFACE_OPACITY_BUTTON_DISABLED ) ) ) @@ -236,26 +420,38 @@ class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, privat } } - fun themeCheckbox(checkbox: CheckBox) { - withScheme(checkbox) { scheme -> - checkbox.buttonTintList = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_checked), - intArrayOf(android.R.attr.state_checked) - ), - intArrayOf(Color.GRAY, scheme.primary) + fun themeCheckedTextView(vararg checkedTextViews: CheckedTextView) { + withScheme(checkedTextViews[0]) { scheme -> + val colorStateList = buildColorStateList( + -android.R.attr.state_checked to Color.GRAY, + -android.R.attr.state_enabled to Color.GRAY, + android.R.attr.state_checked to scheme.primary + ) + + checkedTextViews.forEach { + it.checkMarkTintList = colorStateList + } + } + } + + fun themeCheckbox(vararg checkboxes: CheckBox) { + withScheme(checkboxes[0]) { scheme -> + val colorStateList = buildColorStateList( + -android.R.attr.state_checked to Color.GRAY, + -android.R.attr.state_enabled to Color.GRAY, + android.R.attr.state_checked to scheme.primary ) + checkboxes.forEach { + it.buttonTintList = colorStateList + } } } fun themeRadioButton(radioButton: RadioButton) { withScheme(radioButton) { scheme -> - radioButton.buttonTintList = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_checked), - intArrayOf(android.R.attr.state_checked) - ), - intArrayOf(Color.GRAY, scheme.primary) + radioButton.buttonTintList = buildColorStateList( + -android.R.attr.state_checked to Color.GRAY, + android.R.attr.state_checked to scheme.primary ) } } @@ -264,16 +460,11 @@ class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, privat withScheme(editText) { scheme -> // TODO check API-level compatibility // editText.background.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) - editText.backgroundTintList = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_focused), - intArrayOf(android.R.attr.state_focused) - ), - intArrayOf( - scheme.outline, - scheme.primary - ) + editText.backgroundTintList = buildColorStateList( + -android.R.attr.state_focused to scheme.outline, + android.R.attr.state_focused to scheme.primary ) + editText.setHintTextColor(scheme.onSurfaceVariant) editText.setTextColor(scheme.onSurface) } @@ -319,6 +510,23 @@ class AndroidViewThemeUtils @Inject constructor(schemes: MaterialSchemes, privat } while (index != -1) } + // here for backwards compatibility + @SuppressLint("UseSwitchCompatOrMaterialCode") + fun colorSwitch(switch: Switch) { + withScheme(switch) { scheme -> + val colors = SwitchColorUtils.calculateSwitchColors(switch.context, scheme) + DrawableCompat.setTintList(switch.thumbDrawable, colors.thumbColor) + DrawableCompat.setTintList(switch.trackDrawable, colors.trackColor) + } + } + + @Deprecated("Don't do this, implement custom viewThemeUtils instead") + fun primaryColor(activity: Activity): Int { + return withScheme(activity) { scheme -> + scheme.primary + } + } + companion object { private const val ON_SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.38f } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt index c38c6e84..5ac2de01 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt @@ -23,10 +23,21 @@ package com.nextcloud.android.common.ui.theme.utils +import android.content.Context import android.content.res.ColorStateList -import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.appcompat.app.ActionBar +import androidx.appcompat.widget.AppCompatTextView +import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SwitchCompat -import androidx.core.content.res.ResourcesCompat +import androidx.core.app.NotificationCompat +import androidx.core.widget.TextViewCompat import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.theme.MaterialSchemes @@ -36,52 +47,88 @@ import javax.inject.Inject /** * View theme utils for Android extension views (androidx.*) */ -class AndroidXViewThemeUtils @Inject constructor(schemes: MaterialSchemes) : +class AndroidXViewThemeUtils @Inject constructor( + schemes: MaterialSchemes, + private val androidViewThemeUtils: AndroidViewThemeUtils +) : ViewThemeUtilsBase(schemes) { fun colorSwitchCompat(switchCompat: SwitchCompat) { withScheme(switchCompat) { scheme -> + val colors = SwitchColorUtils.calculateSwitchColors(switchCompat.context, scheme) + switchCompat.thumbTintList = colors.thumbColor + switchCompat.trackTintList = colors.trackColor + } + } - val context = switchCompat.context + fun themeSwipeRefreshLayout(swipeRefreshLayout: SwipeRefreshLayout) { + withScheme(swipeRefreshLayout) { scheme -> + swipeRefreshLayout.setColorSchemeColors(scheme.primary) + swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) + } + } - val thumbUncheckedColor = ResourcesCompat.getColor( - context.resources, - R.color.switch_thumb_color_unchecked, - context.theme - ) - val trackUncheckedColor = ResourcesCompat.getColor( - context.resources, - R.color.switch_track_color_unchecked, - context.theme - ) + fun colorPrimaryTextViewElement(textView: AppCompatTextView) { + withScheme(textView) { scheme -> + textView.setTextColor(scheme.primary) + TextViewCompat.setCompoundDrawableTintList(textView, ColorStateList.valueOf(scheme.primary)) + } + } - val trackColor = Color.argb( - SWITCH_COMPAT_TRACK_ALPHA, - Color.red(scheme.primary), - Color.green(scheme.primary), - Color.blue(scheme.primary) - ) + // TODO host the back arrow in this lib instead of passing it everywhere + fun themeActionBar(context: Context, actionBar: ActionBar, title: String, backArrow: Drawable) { + withScheme(context) { scheme -> + val text: Spannable = getColoredSpan(title, scheme.onSurface) + actionBar.title = text + themeActionBar(context, actionBar, backArrow) + } + } - switchCompat.thumbTintList = ColorStateList( - arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), - intArrayOf(scheme.primary, thumbUncheckedColor) - ) + fun themeActionBar(context: Context, actionBar: ActionBar, backArrow: Drawable) { + withScheme(context) { scheme -> + actionBar.setBackgroundDrawable(ColorDrawable(scheme.surface)) + val indicator = androidViewThemeUtils.colorDrawable(backArrow, scheme.onSurface) + actionBar.setHomeAsUpIndicator(indicator) + } + } - switchCompat.trackTintList = ColorStateList( - arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), - intArrayOf(trackColor, trackUncheckedColor) - ) + fun themeActionBarSubtitle(context: Context, actionBar: ActionBar) { + withScheme(context) { scheme -> + actionBar.subtitle = getColoredSpan(actionBar.subtitle.toString(), scheme.onSurfaceVariant) } } - fun themeSwipeRefreshLayout(swipeRefreshLayout: SwipeRefreshLayout) { - withScheme(swipeRefreshLayout) { scheme -> - swipeRefreshLayout.setColorSchemeColors(scheme.primary) - swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) + fun themeToolbarSearchView(searchView: SearchView) { + withScheme(searchView) { scheme -> + // hacky as no default way is provided + val editText = + searchView.findViewById(androidx.appcompat.R.id.search_src_text) + val searchPlate = searchView.findViewById(androidx.appcompat.R.id.search_plate) + val closeButton = searchView.findViewById(androidx.appcompat.R.id.search_close_btn) + val searchButton = searchView.findViewById(androidx.appcompat.R.id.search_button) + editText.setHintTextColor(scheme.onSurfaceVariant) + editText.highlightColor = scheme.inverseOnSurface + editText.setTextColor(scheme.onSurface) + closeButton.setColorFilter(scheme.onSurface) + searchButton.setColorFilter(scheme.onSurface) + searchPlate.setBackgroundColor(scheme.surface) + } + } + + fun themeNotificationCompatBuilder(context: Context, builder: NotificationCompat.Builder) { + withScheme(context) { scheme -> + builder.setColor(scheme.primary) } } - companion object { - private const val SWITCH_COMPAT_TRACK_ALPHA: Int = 77 + private fun getColoredSpan(title: String, color: Int): Spannable { + val text: Spannable = SpannableString(title) + text.setSpan( + ForegroundColorSpan(color), + 0, + text.length, + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ) + return text } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/ColorRole.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/ColorRole.kt new file mode 100644 index 00000000..454aba86 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/ColorRole.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud Android client application + * + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + * + */ + +package com.nextcloud.android.common.ui.theme.utils + +import scheme.Scheme + +/** + * Parameter for library methods so that clients can choose color roles without accessing the Scheme directly + */ +enum class ColorRole(internal val select: (Scheme) -> Int) { + ON_PRIMARY({ it.onPrimary }), + ON_SECONDARY_CONTAINER({ it.onSecondaryContainer }), + ON_SURFACE({ it.onSurface }), + PRIMARY({ it.primary }), + SURFACE({ it.surface }) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt index 258328f1..882e1224 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt @@ -27,6 +27,7 @@ import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.PorterDuff +import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.button.MaterialButton @@ -35,6 +36,7 @@ import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.progressindicator.LinearProgressIndicator +import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textview.MaterialTextView @@ -42,6 +44,7 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.color.ColorUtil import com.nextcloud.android.common.ui.theme.MaterialSchemes import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase +import com.nextcloud.android.common.ui.util.buildColorStateList import scheme.Scheme import javax.inject.Inject @@ -65,39 +68,42 @@ class MaterialViewThemeUtils @Inject constructor(schemes: MaterialSchemes, priva fun themeFAB(fab: FloatingActionButton) { withScheme(fab) { scheme -> - fab.backgroundTintList = ColorStateList.valueOf(scheme.primaryContainer) - fab.imageTintList = ColorStateList.valueOf(scheme.onPrimaryContainer) + fab.backgroundTintList = buildColorStateList( + android.R.attr.state_enabled to scheme.primaryContainer, + -android.R.attr.state_enabled to Color.GRAY + ) + + fab.imageTintList = buildColorStateList( + android.R.attr.state_enabled to scheme.onPrimaryContainer, + -android.R.attr.state_enabled to Color.WHITE + ) } } fun themeCardView(cardView: MaterialCardView) { withScheme(cardView) { scheme -> cardView.backgroundTintList = ColorStateList.valueOf(scheme.surface) + cardView.setStrokeColor( + buildColorStateList( + android.R.attr.state_checked to scheme.primary, + -android.R.attr.state_checked to scheme.outline + ) + ) } } fun colorMaterialTextButton(button: MaterialButton) { withScheme(button) { scheme -> - button.rippleColor = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_pressed) - ), - intArrayOf( - colorUtil.adjustOpacity(scheme.primary, SURFACE_OPACITY_BUTTON_DISABLED) - ) - ) + button.rippleColor = rippleColor(scheme) } } fun colorMaterialButtonText(button: MaterialButton) { withScheme(button) { scheme -> val disabledColor = ContextCompat.getColor(button.context, R.color.disabled_text) - val colorStateList = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) - ), - intArrayOf(scheme.primary, disabledColor) + val colorStateList = buildColorStateList( + android.R.attr.state_enabled to scheme.primary, + -android.R.attr.state_enabled to disabledColor ) button.setTextColor(colorStateList) button.iconTint = colorStateList @@ -106,96 +112,151 @@ class MaterialViewThemeUtils @Inject constructor(schemes: MaterialSchemes, priva fun colorMaterialButtonPrimaryFilled(button: MaterialButton) { withScheme(button) { scheme -> - button.backgroundTintList = - ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) - ), - intArrayOf( - scheme.primary, - colorUtil.adjustOpacity(scheme.onSurface, SURFACE_OPACITY_BUTTON_DISABLED) - ) + button.backgroundTintList = buildColorStateList( + android.R.attr.state_enabled to scheme.primary, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + SURFACE_OPACITY_BUTTON_DISABLED ) + ) - button.setTextColor( - ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) - ), - intArrayOf( - scheme.onPrimary, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) - ) + val contentColorList = buildColorStateList( + android.R.attr.state_enabled to scheme.onPrimary, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + SURFACE_OPACITY_BUTTON_DISABLED ) ) - button.iconTint = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) + button.setTextColor(contentColorList) + button.iconTint = contentColorList + } + } + + fun colorMaterialButtonPrimaryTonal(button: MaterialButton) { + withScheme(button) { scheme -> + button.backgroundTintList = buildColorStateList( + android.R.attr.state_enabled to scheme.secondaryContainer, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + SURFACE_OPACITY_BUTTON_DISABLED ), - intArrayOf( - scheme.onPrimary, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) - ) + -android.R.attr.state_hovered to scheme.onSecondaryContainer, + -android.R.attr.state_focused to scheme.onSecondaryContainer, + -android.R.attr.state_pressed to scheme.onSecondaryContainer ) + + val contentColorList = buildColorStateList( + android.R.attr.state_enabled to scheme.onSecondaryContainer, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + -android.R.attr.state_hovered to scheme.onSecondaryContainer, + -android.R.attr.state_focused to scheme.onSecondaryContainer, + -android.R.attr.state_pressed to scheme.onSecondaryContainer + ) + button.setTextColor(contentColorList) + button.iconTint = contentColorList } } fun colorMaterialButtonPrimaryOutlined(button: MaterialButton) { withScheme(button) { scheme -> - button.strokeColor = ColorStateList.valueOf(scheme.outline) - button.setTextColor( - ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) - ), - intArrayOf( - scheme.primary, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) - ) + val contentColorList = buildColorStateList( + android.R.attr.state_enabled to scheme.primary, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + ON_SURFACE_OPACITY_BUTTON_DISABLED ) ) - button.iconTint = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) + + button.setTextColor(contentColorList) + button.iconTint = contentColorList + button.strokeColor = buildColorStateList( + android.R.attr.state_enabled to scheme.outline, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED ), - intArrayOf( - scheme.primary, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) - ) + -android.R.attr.state_hovered to scheme.outline, + -android.R.attr.state_focused to scheme.primary, + -android.R.attr.state_pressed to scheme.outline ) + + button.strokeWidth = + button.resources.getDimension(R.dimen.outlinedButtonStrokeWidth).toInt() + button.rippleColor = rippleColor(scheme) } } fun colorMaterialButtonPrimaryBorderless(button: MaterialButton) { withScheme(button) { scheme -> - button.setTextColor( - ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) - ), - intArrayOf( - scheme.primary, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) - ) + val contentColorList = buildColorStateList( + android.R.attr.state_enabled to scheme.primary, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onSurface, + ON_SURFACE_OPACITY_BUTTON_DISABLED ) ) - button.iconTint = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_enabled), - intArrayOf(-android.R.attr.state_enabled) + + button.setTextColor(contentColorList) + button.iconTint = contentColorList + } + } + + /** + * text is primary, background is on_primary + */ + fun colorMaterialButtonFilledOnPrimary(button: MaterialButton) { + withScheme(button) { scheme -> + button.backgroundTintList = buildColorStateList( + android.R.attr.state_enabled to scheme.onPrimary, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.surface, + SURFACE_OPACITY_BUTTON_DISABLED ), - intArrayOf( + -android.R.attr.state_hovered to scheme.onPrimary, + -android.R.attr.state_focused to scheme.onPrimary, + -android.R.attr.state_pressed to scheme.onPrimary + ) + + val contentColorList = buildColorStateList( + android.R.attr.state_enabled to scheme.primary, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( scheme.primary, - colorUtil.adjustOpacity(scheme.onSurface, ON_SURFACE_OPACITY_BUTTON_DISABLED) + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + -android.R.attr.state_hovered to scheme.primary, + -android.R.attr.state_focused to scheme.primary, + -android.R.attr.state_pressed to scheme.primary + ) + + button.setTextColor( + contentColorList + ) + + button.iconTint = contentColorList + } + } + + fun colorMaterialButtonOutlinedOnPrimary(button: MaterialButton) { + withScheme(button) { scheme -> + button.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) + val contentColorList = buildColorStateList( + android.R.attr.state_enabled to scheme.onPrimary, + -android.R.attr.state_enabled to colorUtil.adjustOpacity( + scheme.onPrimary, + ON_SURFACE_OPACITY_BUTTON_DISABLED ) ) + + button.setTextColor(contentColorList) + button.iconTint = contentColorList + button.strokeColor = contentColorList + button.strokeWidth = + button.resources.getDimension(R.dimen.outlinedButtonStrokeWidth).toInt() + button.rippleColor = rippleColor(scheme) } } @@ -207,41 +268,34 @@ class MaterialViewThemeUtils @Inject constructor(schemes: MaterialSchemes, priva } } - fun colorCardViewBackground(card: MaterialCardView) { - withScheme(card) { scheme -> - card.setCardBackgroundColor(scheme.surfaceVariant) - } - } + @Deprecated( + "Duplicated, use themeCardView instead", + replaceWith = ReplaceWith("themeCardView(card)") + ) + fun colorCardViewBackground(card: MaterialCardView) = themeCardView(card) fun colorProgressBar(progressIndicator: LinearProgressIndicator) { withScheme(progressIndicator) { scheme -> - progressIndicator.setIndicatorColor(scheme.primary) + colorProgressBar(progressIndicator, scheme.primary) } } + fun colorProgressBar(progressIndicator: LinearProgressIndicator, @ColorInt color: Int) { + progressIndicator.setIndicatorColor(color) + } + fun colorTextInputLayout(textInputLayout: TextInputLayout) { withScheme(textInputLayout) { scheme -> val errorColor = scheme.onSurfaceVariant - val errorColorStateList = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_focused), - intArrayOf(android.R.attr.state_focused) - ), - intArrayOf( - errorColor, - errorColor - ) + val errorColorStateList = buildColorStateList( + -android.R.attr.state_focused to errorColor, + android.R.attr.state_focused to errorColor ) - val coloredColorStateList = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_focused), - intArrayOf(android.R.attr.state_focused) - ), - intArrayOf( - scheme.outline, - scheme.primary - ) + + val coloredColorStateList = buildColorStateList( + -android.R.attr.state_focused to scheme.outline, + android.R.attr.state_focused to scheme.primary ) textInputLayout.setBoxStrokeColorStateList(coloredColorStateList) @@ -249,6 +303,14 @@ class MaterialViewThemeUtils @Inject constructor(schemes: MaterialSchemes, priva textInputLayout.setErrorTextColor(errorColorStateList) textInputLayout.boxStrokeErrorColor = errorColorStateList textInputLayout.defaultHintTextColor = coloredColorStateList + + textInputLayout.editText?.highlightColor = scheme.primary + } + } + + fun themeTabLayout(tabLayout: TabLayout) { + withScheme(tabLayout) { scheme -> + colorTabLayout(tabLayout, scheme) } } @@ -261,24 +323,17 @@ class MaterialViewThemeUtils @Inject constructor(schemes: MaterialSchemes, priva fun colorTabLayout(tabLayout: TabLayout, scheme: Scheme) { tabLayout.setSelectedTabIndicatorColor(scheme.primary) - tabLayout.tabTextColors = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_selected), - intArrayOf(-android.R.attr.state_selected) - ), - intArrayOf( - scheme.primary, - ContextCompat.getColor(tabLayout.context, R.color.high_emphasis_text) - ) - ) - tabLayout.tabRippleColor = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_pressed) - ), - intArrayOf( - colorUtil.adjustOpacity(scheme.primary, SURFACE_OPACITY_BUTTON_DISABLED) + val tabContentColors = buildColorStateList( + android.R.attr.state_selected to scheme.primary, + -android.R.attr.state_selected to ContextCompat.getColor( + tabLayout.context, + R.color.high_emphasis_text ) ) + + tabLayout.tabTextColors = tabContentColors + tabLayout.tabIconTint = tabContentColors + tabLayout.tabRippleColor = rippleColor(scheme) } fun colorChipBackground(chip: Chip) { @@ -304,8 +359,24 @@ class MaterialViewThemeUtils @Inject constructor(schemes: MaterialSchemes, priva } } + fun themeSnackbar(snackbar: Snackbar) { + withScheme(snackbar.context) { scheme -> + snackbar.setBackgroundTint(scheme.inverseSurface) + snackbar.setActionTextColor(scheme.inversePrimary) + snackbar.setTextColor(scheme.inverseOnSurface) + } + } + + private fun rippleColor(scheme: Scheme) = buildColorStateList( + android.R.attr.state_pressed to colorUtil.adjustOpacity( + scheme.primary, + SURFACE_OPACITY_BUTTON_DISABLED + ) + ) + companion object { private const val SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.12f + private const val ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED: Float = 0.12f private const val ON_SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.38f } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt new file mode 100644 index 00000000..8de89a5c --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud Android client application + * + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + * + */ + +package com.nextcloud.android.common.ui.theme.utils + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import androidx.core.content.res.ResourcesCompat +import com.nextcloud.android.common.ui.R +import scheme.Scheme + +/** + * To be used to calculate color lists for both Switch and SwitchCompat + */ +internal object SwitchColorUtils { + + private const val SWITCH_COMPAT_TRACK_ALPHA: Int = 77 + + data class SwitchColors( + val thumbColor: ColorStateList, + val trackColor: ColorStateList + ) + + fun calculateSwitchColors(context: Context, scheme: Scheme): SwitchColors { + val thumbUncheckedColor = ResourcesCompat.getColor( + context.resources, + R.color.switch_thumb_color_unchecked, + context.theme + ) + val trackUncheckedColor = ResourcesCompat.getColor( + context.resources, + R.color.switch_track_color_unchecked, + context.theme + ) + + val trackColor = Color.argb( + SWITCH_COMPAT_TRACK_ALPHA, + Color.red(scheme.primary), + Color.green(scheme.primary), + Color.blue(scheme.primary) + ) + + return SwitchColors( + thumbColor = ColorStateList( + arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), + intArrayOf(scheme.primary, thumbUncheckedColor) + ), + trackColor = ColorStateList( + arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), + intArrayOf(trackColor, trackUncheckedColor) + ) + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/ColorStateListUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/ColorStateListUtils.kt new file mode 100644 index 00000000..05ea3ae8 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/ColorStateListUtils.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Android Common Library + * + * Copyright (C) 2022 Nextcloud GmbH + * + * 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 com.nextcloud.android.common.ui.util + +import android.content.res.ColorStateList + +/** + * First element of each pair is @AttRes (can be negated) and second one is @ColorInt + */ +fun buildColorStateList(vararg pairs: Pair): ColorStateList { + val stateArray = pairs.map { intArrayOf(it.first) }.toTypedArray() + val colorArray = pairs.map { it.second }.toIntArray() + return ColorStateList(stateArray, colorArray) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/PlatformThemeUtil.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/PlatformThemeUtil.kt index 973eea93..04603205 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/util/PlatformThemeUtil.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/PlatformThemeUtil.kt @@ -25,6 +25,7 @@ import android.content.Context import android.content.res.Configuration object PlatformThemeUtil { + @JvmStatic fun isDarkMode(context: Context): Boolean = when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true diff --git a/ui/src/main/res/values/dimens.xml b/ui/src/main/res/values/dimens.xml index 354fce31..12e08de0 100644 --- a/ui/src/main/res/values/dimens.xml +++ b/ui/src/main/res/values/dimens.xml @@ -19,4 +19,5 @@ 28dp + 1dp