diff --git a/README.md b/README.md index c682564d6..08a277c8b 100644 --- a/README.md +++ b/README.md @@ -120,3 +120,4 @@ And: - Menu - Navigation View - Text Field +- [Shortcut](doc/shortcut.md) diff --git a/build.gradle b/build.gradle index 046bda74a..86231894f 100644 --- a/build.gradle +++ b/build.gradle @@ -51,4 +51,5 @@ ext { materialVersion = '1.1.0' espressoIntentsVersion = '3.1.0' mockKVersion = '1.10.0' + coreKTXVersion = '1.3.0' } diff --git a/designsystem/build.gradle b/designsystem/build.gradle index 84ad96ed9..556eb0b08 100644 --- a/designsystem/build.gradle +++ b/designsystem/build.gradle @@ -55,13 +55,14 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.google.android.material:material:$rootProject.materialVersion" implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayout" - + implementation "androidx.core:core-ktx:$rootProject.coreKTXVersion" testImplementation "junit:junit:$rootProject.junitVersion" testImplementation "androidx.test.ext:truth:$rootProject.truthVersion" testImplementation "androidx.test:runner:$rootProject.androidXTestVersion" testImplementation "androidx.test:core-ktx:$rootProject.testCoreKtxVersion" testImplementation "androidx.test.ext:junit:$rootProject.androidxJunitVersion" testImplementation "org.robolectric:robolectric:$rootProject.robolectricVersion" + testImplementation "io.mockk:mockk:$rootProject.mockKVersion" } androidExtensions { diff --git a/designsystem/src/main/kotlin/com/natura/android/appbar/AppBar.kt b/designsystem/src/main/kotlin/com/natura/android/appbar/AppBar.kt index 59c9e4fa4..a4db7b9fa 100644 --- a/designsystem/src/main/kotlin/com/natura/android/appbar/AppBar.kt +++ b/designsystem/src/main/kotlin/com/natura/android/appbar/AppBar.kt @@ -1,101 +1,124 @@ package com.natura.android.appbar -import android.app.Activity import android.content.Context -import android.graphics.Color -import android.os.Build +import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.util.DisplayMetrics import android.util.TypedValue +import android.view.Gravity import android.view.View +import android.view.ViewGroup import android.view.WindowManager +import android.widget.ImageView import androidx.appcompat.widget.Toolbar import com.natura.android.R -import kotlin.math.sqrt +import com.natura.android.ext.setVisibilityFromBoolean +import com.natura.android.badge.BadgeDrawable -// WIP class AppBar(context: Context, attrs: AttributeSet) : Toolbar(context, attrs) { - var color: Int + + private lateinit var badgeDrawable: BadgeDrawable + private var showLogo: Boolean + private val logo: ImageView init { - context - .theme - .obtainStyledAttributes(attrs, R.styleable.AppBar, 0, 0) - .apply { - try { - color = this.getInt(R.styleable.AppBar_appBarType, 0) - } finally { - recycle() - } - } - } + logo = createLogo(context, attrs) - override fun onAttachedToWindow() { - super.onAttachedToWindow() - setStatusBarStyle() + val typedValue = context.obtainStyledAttributes(attrs, R.styleable.AppBar) + showLogo = typedValue.getBoolean(R.styleable.AppBar_showLogo, false) + setLogoVisibility() - this.setBackgroundColor(getThemeColor(getMainColorFromAttrs())) - this.setTitleTextColor(getThemeColor(getMainOnColorFromAttrs())) + addView(logo) - if (isOnColorTooLight(getThemeColor(getMainOnColorFromAttrs()))) { - setStatusBarIconsLighter() - } else { - setStatusBarIconsDarker() + contentInsetStartWithNavigation = 0 + elevation = getElevationFromTheme(context) + + typedValue.recycle() + } + + private fun setLogoVisibility() { + logo.setVisibilityFromBoolean(showLogo) + + if (showLogo) { + title = "" } } - private fun isOnColorTooLight(color: Int): Boolean { - val rgb = intArrayOf(Color.red(color), Color.green(color), Color.blue(color)) - val brightness = sqrt(rgb[0] * rgb[0] * .241 + (rgb[1] * rgb[1] * .691) + rgb[2] * rgb[2] * .068).toInt() - return brightness >= 200 + fun showLogo() { + showLogo = true + setLogoVisibility() } - private fun setStatusBarStyle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + fun hideLogo() { + showLogo = false + setLogoVisibility() + } + + fun addMenuIconBadge(menuIcon: Drawable, initBadgeValue: Int) { + badgeDrawable = BadgeDrawable(context, initBadgeValue, menuIcon) + } - val context = context as Activity - val window = context.window - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - window.statusBarColor = getThemeColor(getMainColorFromAttrs()) + fun updateBadgeValue(value: Int) { + if (this::badgeDrawable.isInitialized) { + badgeDrawable.updateBadgeDrawable(value) } } - private fun getMainColorFromAttrs(): Int { - return when (color) { - 1 -> R.attr.colorPrimary - 2 -> R.attr.colorSecondary - 3 -> android.R.color.transparent - else -> R.attr.colorSurface - } + private fun createLogo(context: Context, attrs: AttributeSet): ImageView { + val imageView = ImageView(context) + imageView.setImageResource(getLogoResId(context, attrs)) + val logoWidth = getLogoWidthFromTheme(context) + imageView.layoutParams = + LayoutParams(logoWidth, ViewGroup.LayoutParams.WRAP_CONTENT, getLogoAlign(context)) + imageView.visibility = View.GONE + return imageView } - private fun getMainOnColorFromAttrs(): Int { - return when (color) { - 1 -> R.attr.colorOnPrimary - 2 -> R.attr.colorOnSecondary - 3 -> R.attr.colorOnSurface - else -> R.attr.colorOnSurface + private fun getLogoAlign(context: Context): Int { + return if (getWindowWidthInPx(context) < MINIMUM_SCREEN_SIZE_FOR_CENTRALIZED_LOGO) { + Gravity.START + } else { + Gravity.CENTER } } - private fun getThemeColor(colorAttr: Int): Int { - val outValue = TypedValue() - context.theme.resolveAttribute(colorAttr, outValue, true) - return outValue.data + private fun getLogoWidthFromTheme(context: Context): Int { + val typedValue = TypedValue() + if (context.theme.resolveAttribute(R.attr.sizeHugeX, typedValue, true)) { + return TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics) + } + + return ViewGroup.LayoutParams.WRAP_CONTENT } - private fun setStatusBarIconsDarker() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val context = context as Activity - val window = context.window - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + private fun getElevationFromTheme(context: Context): Float { + val typedValue = TypedValue() + if (context.theme.resolveAttribute(R.attr.elevation02, typedValue, true)) { + return TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics).toFloat() } + + return 0f } - private fun setStatusBarIconsLighter() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val context = context as Activity - val window = context.window - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + private fun getLogoResId(context: Context, attrs: AttributeSet): Int { + val typedValue = context.theme + .obtainStyledAttributes(attrs, intArrayOf(R.attr.logoHorizontal), 0, 0) + return typedValue.getResourceId(0, 0) + } + + private fun getWindowWidthInPx(context: Context): Int { + return try { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val metrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(metrics) + + metrics.widthPixels + } catch (ex: Exception) { + MINIMUM_SCREEN_SIZE_FOR_CENTRALIZED_LOGO } } + + companion object { + private const val MINIMUM_SCREEN_SIZE_FOR_CENTRALIZED_LOGO = 361 + } } diff --git a/designsystem/src/main/kotlin/com/natura/android/exceptions/MissingThemeException.kt b/designsystem/src/main/kotlin/com/natura/android/exceptions/MissingThemeException.kt new file mode 100644 index 000000000..404e4c8d5 --- /dev/null +++ b/designsystem/src/main/kotlin/com/natura/android/exceptions/MissingThemeException.kt @@ -0,0 +1,6 @@ +package com.natura.android.exceptions + +import java.lang.IllegalArgumentException + +class MissingThemeException : + IllegalArgumentException("⚠️ ⚠️ Missing DS Theme. You are using a DS component without setting a DS Theme. You MUST set a DS theme at the component or in a parent view") diff --git a/designsystem/src/main/kotlin/com/natura/android/extensions/TextFieldExtensions.kt b/designsystem/src/main/kotlin/com/natura/android/extensions/TextFieldExtensions.kt new file mode 100644 index 000000000..f54147d94 --- /dev/null +++ b/designsystem/src/main/kotlin/com/natura/android/extensions/TextFieldExtensions.kt @@ -0,0 +1,12 @@ +package com.natura.android.extensions + +import android.os.Build +import android.widget.TextView + +fun TextView.setAppearance(textAppearance: Int) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + this.setTextAppearance(context, textAppearance) + } else { + this.setTextAppearance(textAppearance) + } +} diff --git a/designsystem/src/main/kotlin/com/natura/android/shortcut/Shortcut.kt b/designsystem/src/main/kotlin/com/natura/android/shortcut/Shortcut.kt new file mode 100644 index 000000000..46031f0e2 --- /dev/null +++ b/designsystem/src/main/kotlin/com/natura/android/shortcut/Shortcut.kt @@ -0,0 +1,179 @@ +package com.natura.android.shortcut + +import android.content.Context +import android.content.res.TypedArray +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.content.res.getIntOrThrow +import androidx.core.content.res.getResourceIdOrThrow +import androidx.core.content.res.getStringOrThrow +import androidx.core.graphics.drawable.DrawableCompat +import com.natura.android.R +import com.natura.android.exceptions.MissingThemeException +import com.natura.android.extensions.setAppearance + +class Shortcut @JvmOverloads constructor( + context: Context, + private val attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private var labelAttribute: String? = null + private var typeAttribute: Int? = null + private var backgroundColorResourceAttribute = 0 + private var iconColorResourceAttribute = 0 + private var labelTextAppearanceResourceAttribute = 0 + private var iconAttribute: Int? = null + private var shortcutAttributesArray: TypedArray + + private val labelContainer by lazy { findViewById(R.id.shortCutLabel) } + private val backgroundContainer by lazy { findViewById(R.id.shortcutBackground) } + private val iconContainer by lazy { findViewById(R.id.shortCutIcon) } + + init { + try { + View.inflate(context, R.layout.shortcut, this) + } catch (e: Exception) { + throw (MissingThemeException()) + } + + shortcutAttributesArray = context.obtainStyledAttributes(attrs, R.styleable.Shortcut) + + getShortcutAttributes() + getAttributesFromTheme() + configureShortCutByType(typeAttribute) + + shortcutAttributesArray.recycle() + } + + fun setLabel(text: String?) { + labelContainer.text = text + labelContainer.setAppearance(labelTextAppearanceResourceAttribute) + } + + fun getLabel(): CharSequence? { + return labelContainer.text + } + + fun getType(): Int? = typeAttribute + + fun setIcon(icon: Int?) { + icon?.apply { + iconContainer.setImageResource(icon) + iconContainer.setColorFilter(ContextCompat.getColor(context, iconColorResourceAttribute), android.graphics.PorterDuff.Mode.SRC_IN) + } + } + + fun getIcon(): ImageView { + return iconContainer + } + + private fun getAttributesFromTheme() { + try { + if (typeAttribute == CONTAINED) { + setContainedTypeAttributes() + } else { + setOutlinedTypeAttributes() + } + } catch (e: Exception) { + throw (MissingThemeException()) + } + } + + private fun setContainedTypeAttributes() { + context + .theme + .obtainStyledAttributes( + attrs, + R.styleable.Shortcut, + R.attr.shortcutContained, + 0 + ) + .apply { + backgroundColorResourceAttribute = this.getResourceIdOrThrow(R.styleable.Shortcut_colorBackground) + iconColorResourceAttribute = this.getResourceIdOrThrow(R.styleable.Shortcut_colorIcon) + labelTextAppearanceResourceAttribute = this.getResourceIdOrThrow(R.styleable.Shortcut_labelAppearance) + } + } + + private fun setOutlinedTypeAttributes() { + context + .theme + .obtainStyledAttributes(attrs, R.styleable.Shortcut, R.attr.shortcutOutlined, 0) + .apply { + backgroundColorResourceAttribute = this.getResourceIdOrThrow(R.styleable.Shortcut_colorBackground) + iconColorResourceAttribute = this.getResourceIdOrThrow(R.styleable.Shortcut_colorIcon) + labelTextAppearanceResourceAttribute = this.getResourceIdOrThrow(R.styleable.Shortcut_labelAppearance) + } + } + + private fun getShortcutAttributes() { + getLabelAttribute() + getIconAttribute() + getTypeAttribute() + } + + private fun getTypeAttribute() { + try { + typeAttribute = shortcutAttributesArray.getIntOrThrow(R.styleable.Shortcut_type) + } catch (e: Exception) { + throw (IllegalArgumentException("⚠️ ⚠️ Missing shortcut required argument. You MUST set the shortcut type(contained or outlined).", e)) + } + } + + private fun getIconAttribute() { + try { + iconAttribute = shortcutAttributesArray.getResourceIdOrThrow(R.styleable.Shortcut_icon) + } catch (e: Exception) { + throw (IllegalArgumentException("⚠️ ⚠️ Missing shortcut required argument. You MUST set the shortcut icon(drawable).", e)) + } + } + + private fun getLabelAttribute() { + try { + labelAttribute = shortcutAttributesArray.getStringOrThrow(R.styleable.Shortcut_textLabel) + } catch (e: Exception) { + throw (IllegalArgumentException("⚠️ ⚠️ Missing shortcut required argument. You MUST set the shortcut label(string).", e)) + } + } + + private fun configureShortCutByType(type: Int?) { + type?.apply { + setLabel(labelAttribute) + setIcon(iconAttribute) + + when (this) { + CONTAINED -> setBackgroundContained() + OUTLINED -> setBackgroundOutlined() + } + } + } + + private fun setBackgroundContained() { + val background = resources.getDrawable(R.drawable.shortcut_background, null) + val backgroundWrap = DrawableCompat.wrap(background).mutate() + DrawableCompat.setTint(backgroundWrap, ContextCompat.getColor(context, backgroundColorResourceAttribute)) + + backgroundContainer.background = background + } + + private fun setBackgroundOutlined() { + val background = resources.getDrawable(R.drawable.shortcut_background, null) as GradientDrawable + background.setColor(ContextCompat.getColor(context, backgroundColorResourceAttribute)) + background.setStroke(1, ContextCompat.getColor(context, iconColorResourceAttribute)) + + backgroundContainer.background = background + backgroundContainer.elevation = 0F + } + + companion object { + const val OUTLINED = 0 + const val CONTAINED = 1 + } +} diff --git a/designsystem/src/main/res/drawable/shortcut_background.xml b/designsystem/src/main/res/drawable/shortcut_background.xml new file mode 100644 index 000000000..52cec36d0 --- /dev/null +++ b/designsystem/src/main/res/drawable/shortcut_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/designsystem/src/main/res/drawable/shortcut_ripple_background.xml b/designsystem/src/main/res/drawable/shortcut_ripple_background.xml new file mode 100644 index 000000000..ab07823b7 --- /dev/null +++ b/designsystem/src/main/res/drawable/shortcut_ripple_background.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/designsystem/src/main/res/layout/custom_app_bar.xml b/designsystem/src/main/res/layout/custom_app_bar.xml deleted file mode 100644 index 732be60b7..000000000 --- a/designsystem/src/main/res/layout/custom_app_bar.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/designsystem/src/main/res/layout/shortcut.xml b/designsystem/src/main/res/layout/shortcut.xml new file mode 100644 index 000000000..e013a58cb --- /dev/null +++ b/designsystem/src/main/res/layout/shortcut.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/designsystem/src/main/res/menu/custom_menu.xml b/designsystem/src/main/res/menu/custom_menu.xml index fd7771753..b6d541e46 100644 --- a/designsystem/src/main/res/menu/custom_menu.xml +++ b/designsystem/src/main/res/menu/custom_menu.xml @@ -13,4 +13,5 @@ android:icon="@drawable/icon_base_badge" android:title="notification" app:showAsAction="always" /> + \ No newline at end of file diff --git a/designsystem/src/main/res/values/ds_attrs.xml b/designsystem/src/main/res/values/ds_attrs.xml index f334585c8..4fb2a4c64 100644 --- a/designsystem/src/main/res/values/ds_attrs.xml +++ b/designsystem/src/main/res/values/ds_attrs.xml @@ -1,13 +1,19 @@ + + + + + + + + + + + - - - - - - + diff --git a/designsystem/src/main/res/values/ds_base_attrs.xml b/designsystem/src/main/res/values/ds_base_attrs.xml index a1e6beedb..696fe08f9 100644 --- a/designsystem/src/main/res/values/ds_base_attrs.xml +++ b/designsystem/src/main/res/values/ds_base_attrs.xml @@ -26,6 +26,7 @@ + @@ -108,4 +109,8 @@ + + + + \ No newline at end of file diff --git a/designsystem/src/main/res/values/ds_base_themes.xml b/designsystem/src/main/res/values/ds_base_themes.xml index afff09e31..f90a7303d 100644 --- a/designsystem/src/main/res/values/ds_base_themes.xml +++ b/designsystem/src/main/res/values/ds_base_themes.xml @@ -28,6 +28,7 @@ #51333333 #51777777 + #3D000000 @dimen/ds_spacing_none @dimen/ds_spacing_micro @@ -116,6 +117,9 @@ @style/Theme.DS.Toolbar @style/Theme.DS.Dialog.Standard + + @style/Widget.DS.Shortcut.Contained + @style/Widget.DS.Shortcut.Outlined \ No newline at end of file diff --git a/designsystem/src/main/res/values/styles.xml b/designsystem/src/main/res/values/styles.xml index b9b65efb7..4c406845a 100644 --- a/designsystem/src/main/res/values/styles.xml +++ b/designsystem/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ - + - + + + + + + + + + + +