Aquí tenemos una aplicación. Serio, grande, adulto. Prácticamente sin estilos, pero sin desorden; usamos widgets de AppCompat para nosotros, pero ya hemos ajustado el tema de Material Design Components (MDC) y estamos pensando en una migración completa.
Y de repente hay una tarea para un rediseño completo. Y el nuevo diseño tiene la misma lógica empresarial que el anterior. Los componentes son nuevos, las fuentes no estándar, los colores (excepto los corporativos) son diferentes. En general, nos damos cuenta de que es hora de pasar a MDC.
Pero no todo es tan sencillo:
Se supone que el rediseño es poco sistemático. Es decir, la aplicación contendrá ambas pantallas con la apariencia antigua y nueva.
Los colores y la tipografía del nuevo diseño son diferentes de los que recomienda el MDC. Aunque los principios de nomenclatura son similares
La capa de presentación se divide en módulos de interfaz de usuario separados. Y algunos de ellos son utilizados por otra aplicación. Teniendo en cuenta que prescindimos de los estilos, para el estilo en dichos módulos, algunas propiedades están ocultas detrás de los atributos: colores, estilos de texto, cadenas y mucho más.
Existe un esquema establecido sobre cómo trabajar con los módulos de interfaz de usuario anteriores. En particular con atributos. Esto significa también con colores, estilos de texto, cadenas y más. Y con MDC, me gustaría usar estilos
Además, comparto mi experiencia sobre cómo hacer frente a estas dificultades: cómo, al pasar a MDC, estilizar parcialmente una aplicación de Android con módulos de interfaz de usuario independientes, abstraerse del diseño del sistema y no romper nada. Bono: asesoramiento y análisis de las dificultades que encontré.
Acerca de los módulos de interfaz de usuario
Hay módulos de interfaz de usuario. Son independientes del proyecto. Acuéstese por separado de él.
Hay un módulo raíz dentro de cada uno de los proyectos. Llamémoslo presentación central . Depende de los módulos de interfaz de usuario que se utilicen en esta aplicación. Los módulos están conectados como una dependencia normal de Gradle.
Surge la pregunta. ¿Cómo estilizar algo? En resumen, usando atributos. Dentro de cada uno de estos módulos de interfaz de usuario, se definen los atributos utilizados, que deben ser implementados por el tema de la aplicación:
<resources>
<!-- src -->
<attr name = "someUiModuleBackgroundSrc" format = "reference" />
<!-- string -->
<attr name = "someUiModuleTitleString" format = "reference" />
<attr name = "someUiModuleErrorString" format = "reference" />
<!-- textAppearance -->
<attr name = "someUiModuleTextAppearance1" format = "reference" />
<attr name = "someUiModuleTextAppearance2" format = "reference" />
<attr name = "someUiModuleTextAppearance3" format = "reference" />
<attr name = "someUiModuleTextAppearance4" format = "reference" />
<attr name = "someUiModuleTextAppearance5" format = "reference" />
<attr name = "someUiModuleTextAppearance6" format = "reference" />
<attr name = "someUiModuleTextAppearance7" format = "reference" />
<attr name = "someUiModuleTextAppearance8" format = "reference" />
<!-- color -->
<attr name = "someUiModuleColor1" format = "reference" />
<attr name = "someUiModuleColor2" format = "reference" />
</resources>
:
<androidx.appcompat.widget.AppCompatTextView
android:background = "?someUiModuleBackgroundSrc"
android:text = "?someUiModuleErrorString"
android:textAppearance = "?someUiModuleTextAppearance5"
...
/>
"" ()
. , . , , , .
, :
MDC , MDC. AppCompat'a. framework MDC, :
<TextView ... /><!-- Bad --> <androidx.appcompat.widget.AppCompatTextView ... /><!-- Bad --> <com.google.android.material.textview.MaterialTextView ... /><!-- Good -->
(, , ) ui - (, v2)
- View. , View (
style
xml,defStyleAttr
), . :
<!-- Good --> <com.google.android.material.appbar.MaterialToolbar style = "?toolbarStyleV2" /> <!-- Bad --> <com.google.android.material.appbar.MaterialToolbar android:background = "?primaryColorV2" />
. . :
<item name = "filledTextInputStyleV2">@style/V2.Widget.MyFancyApp.TextInputLayout.Filled</item> <!-- Bad --> <item name = "searchTextInputStyleV2">@style/V2.Widget.MyFancyApp.TextInputLayout.Search</item> <!-- Good --> <item name = "blackOutlinedButtonStyleV2">@style/V2.Widget.MyFancyApp.Button.BlackOutlined</item> <!-- Bad --> <item name = "primaryButtonStyleV2">@style/V2.Widget.MyFancyApp.Button.Primary</item> <!-- Good --> <item name = "secondaryButtonStyleV2">@style/V2.Widget.MyFancyApp.Button.Secondary</item> <!-- Good --> <item name = "textButtonStyleV2">@style/V2.Widget.MyFancyApp.Button.Text</item> <!-- Ok. Based on Figma component name -->
, , core-presentation
:
. ,
UI
ui -
: ; . ?
. , TextView
. ? . . , . TextView
. , MDC , - :
While TextAppearance does support android:textColor, MDC tends to separate concerns by specifying this separately in the main widget styles
:
<item name = "v2TextStyleGiftItemPrice">@style/V2.Widget.MyFancyApp.TextView.GiftItemPrice</item>
<item name = "v2TextStyleGiftItemName">@style/V2.Widget.MyFancyApp.TextView.GiftItemName</item>
...
<style name = "V2.Widget.MyFancyApp.TextView.GiftItemPrice">
<item name = "android:textAppearance">?v2TextAppearanceCaption1</item>
<item name = "android:textColor">?v2ColorOnPrimary</item>
</style>
<style name = "V2.Widget.MyFancyApp.TextView.GiftItemName">
<item name = "android:textAppearance">?v2TextAppearanceCaption1</item>
<item name = "android:textColor">?v2ColorOnPrimary</item>
<item name = "textAllCaps">true</item>
<item name = "android:background">?v2ColorPrimary</item>
</style>
...
<com.google.android.material.textview.MaterialTextView
style = "?v2TextStyleGiftItemPrice"
...
/>
<com.google.android.material.textview.MaterialTextView
style = "?v2TextStyleGiftItemName"
...
/>
, , v2 (, primaryButtonStyleV2
), - (v2TextStyleGiftItemName
). , IDE.
, ui :
<resources>
<!-- -->
<attr name = "cardStyleV2" format = "reference" />
<attr name = "appBarStyleV2" format = "reference" />
<attr name = "toolbarStyleV2" format = "reference" />
<attr name = "primaryButtonStyleV2" format = "reference" />
...
<!-- TextView -->
<attr name = "v2TextStyleGiftCategoryTitle" format = "reference" />
<attr name = "v2TextStyleGiftItemPrice" format = "reference" />
<attr name = "v2TextStyleSearchSuggestion" format = "reference" />
<attr name = "v2TextStyleNoResultsTitle" format = "reference" />
...
<!-- -->
<attr name = "ic16CreditV2" format = "reference" />
<attr name = "ic24CloseV2" format = "reference" />
<attr name = "ic48GiftSentV2" format = "reference" />
...
<!-- -->
<attr name = "shopTitleStringV2" format = "reference" />
<attr name = "shopSearchHintStringV2" format = "reference" />
<attr name = "noResultsStringV2" format = "reference" />
...
<!-- styleable View -->
<declare-styleable name = "ShopPriceSlider">
<attr name = "maxPrice" format = "integer" />
</declare-styleable>
</resources>
. . , .
, TextView
, , ( ).
, , , . .
android:background
, - ? -. . - .
:
<style name = "V2.Widget.MyFancyApp.TextView.GiftItemName">
<item name = "android:textAppearance">?v2TextAppearanceCaption1</item>
<item name = "android:textColor">?v2ColorOnPrimary</item>
</style>
<style name = "V2.Widget.MyFancyApp.Button.Primary" parent = "Widget.MaterialComponents.Button">
...
</style>
<style name = "V2.Widget.MyFancyApp.Button.Primary.Price">
...
<item name = "icon">?ic16CreditV2</item>
</style>
, (android:textAppearance
) . . core-presentation, , , ( @color/
, @style/
, @drawable/
). ?
: . . :
( , ) .
"" (Halloween, Christmas, Easter ). . , , -
, ,
MaterialThemeOverlay
, . android:theme
materialThemeOverlay
, MaterialThemeOverlay.wrap(...)
.
- xml:
<item name = "achievementLevelBarStyleV2">@style/V2.Widget.MyFancyApp.AchievementLevelBar</item>
<style name = "V2.Widget.MyFancyApp.AchievementLevelBar" parent = "">
<item name = "materialThemeOverlay">@style/V2.ThemeOverlay.MyFancyApp.AchievementLevelBar</item>
</style>
View:
class AchievementLevelBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.achievementLevelBarStyleV2
) : LinearLayoutCompat(MaterialThemeOverlay.wrap(context, attrs, defStyleAttr, 0), attrs, defStyleAttr) {
init {
View.inflate(context, R.layout.achievement_level_bar, this)
...
}
...
}
. - , init {}
context
, . : context
. , materialThemeOverlay
, context
getContext()
. MaterialButton
:
public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
// Ensure we are using the correctly themed context rather than the context that was passed in.
context = getContext();
( Kotlin, Lint name shadowing. )
Light status bar
status bar StatusBarView
. , ( edge-to-edge), . , .
, status bar translucent. : - overlay ( ), - . status bar (light): background .
, light status bar translucent StatusBarView
. :
light status bar 23 SDK ( ). , , translucent status bar ( )
Translucent status bar
FLAG_TRANSLUCENT_STATUS
; overlay ( light) -FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
-
fun setLightStatusBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
var flags = window.decorView.systemUiVisibility
flags = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
window.decorView.systemUiVisibility = flags
}
}
fun clearLightStatusBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
var flags = window.decorView.systemUiVisibility
flags = flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
window.decorView.systemUiVisibility = flags
}
}
FLAG_TRANSLUCENT_STATUS
StatusBarView
status bar. :
class StatusBarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
init {
...
systemUiVisibility = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
}
StatusBarView
light status bar,statusBarColor
, light / translucent status bar
StatusBarView
Color State List (CSL)
MDC - CSL. , 23 SDK CSL . android:alpha
. , .
:
color/v2_on_background_20.xml
<selector xmlns:android = "http://schemas.android.com/apk/res/android">
<item android:alpha = "0.20" android:color = "?v2ColorOnBackground" />
</selector>
, , @color/
. , CSL - . v2ColorOnBackground
. CSL v2ColorOnBackground
20% :
<color name = "black">#000000</color> <!-- v2ColorOnBackground -->
<color name = "black_20">#33000000</color> <!-- v2ColorOnBackground 20% opacity -->
, :
, 23 SDK . , MDC 21 . , CSL (, View ), MaterialResources.getColorStateList(). Restricted API
,
CSL
android:background
. :
<style name = "V2.Widget.MyFancyApp.Divider" parent = "">
<item name = "android:background">@drawable/v2_rect</item>
<item name = "android:backgroundTint">@color/v2_on_background_15</item>
...
</style>
android:background
. </shape>
xml. v2_rect.xml - . MDC . .
, ShapeableImageView
( MaterialCardView
)? . :
<com.google.android.material.imageview.ShapeableImageView
style = "?shimmerStyleV2"
...
/>
<item name = "shimmerStyleV2">@style/V2.Widget.MyFancyApp.Shimmer</item>
<style name = "V2.Widget.MyFancyApp.Shimmer">
<item name = "srcCompat">@drawable/v2_rect</item>
<item name = "tint">@color/v2_on_background_15</item>
<item name = "shapeAppearance">@style/V2.ShapeAppearance.MyFancyApp.SmallComponent.Shimmer</item>
</style>
ViewGroup
:
<com.google.android.material.appbar.AppBarLayout
style = "?appBarStyleV2"
...
>
<my.magic.path.StatusBarView
style = "?statusBarStyleV2"
...
/>
<com.google.android.material.appbar.MaterialToolbar
style = "?toolbarStyleV2"
...
/>
</com.google.android.material.appbar.AppBarLayout>
, . , .
. . : ? - , AppBarLayout
( secondaryAppBarStyleV2
). ThemeOverlay:
<item name = "secondaryAppBarStyleV2">@style/V2.Widget.MyFancyApp.AppBarLayout.Secondary</item>
<style name = "V2.Widget.MyFancyApp.AppBarLayout.Secondary">
<item name = "materialThemeOverlay">@style/V2.ThemeOverlay.MyFancyApp.AppBarLayout.Secondary</item>
...
</style>
<style name = "V2.ThemeOverlay.MyFancyApp.AppBarLayout.Secondary" parent = "">
<item name = "statusBarStyleV2">@style/V2.Widget.MyFancyApp.StatusBar.Secondary</item>
<item name = "toolbarStyleV2">@style/V2.Widget.MyFancyApp.Toolbar.Secondary</item>
</style>
, ViewGroup. , View. , - View ( ) ViewGroup, , ThemeOverlay ViewGroup.
MaterialToolbar Toolbar AppCompat
framework inflate MDC. MDC, ( ) framework AppCompat. :
<!-- -->
<Toolbar
...
/>
<!-- -->
<androidx.appcompat.widget.Toolbar
...
/>
- . : MaterialToolbar
, - Toolbar
AppCompat.
. MaterialToolbar
navigationIconTint
. Toolbar
AppCompat. , , navigationIcon Toolbar
- navigationIconTint
. MaterialToolbar
.
Material Design Guidelines, Dense text fields. TextInputLayout
40dp. (Widget.MaterialComponents.TextInputLayout.*.Dense
). ( Guidelines) ( ) ; , .
TextInputLayout
, Dense , start icon ... , Dense . , 40dp. , 0 padding
. .
design_text_input_start_icon.xml
, start icon 48dp. , TextInputLayout
40dp android:layout_height
, .
No nos olvidemos de los estilos. Denso se trata de estilo. Por tanto, android:layout_height
en este caso , debe estar dentro del estilo. Y esto es malo porque en cada lugar de uso TextInputLayout
con tal estilo, tendrá que cortar android:layout_height
el marcado (la respuesta a la pregunta de por qué):
<item name = "searchTextInputStyleV2">@style/V2.Widget.MyFancyApp.TextInputLayout.Search</item>
<style name = "V2.Widget.MyFancyApp.TextInputLayout.Search" parent = "Widget.MaterialComponents.TextInputLayout.FilledBox.Dense">
<item name = "android:layout_height">40dp</item>
...
</style>
<!-- -->
<com.google.android.material.textfield.TextInputLayout
style = "?searchTextInputStyleV2"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
/>
<!-- -->
<com.google.android.material.textfield.TextInputLayout
style = "?searchTextInputStyleV2"
android:layout_width = "match_parent"
/>
Quizás esto sea solo un error y en el futuro será posible evitarlo.
En cuanto a mí, resultó ser una buena solución. Tiene sus inconvenientes, pero las ventajas en forma de abstracción del diseño del sistema en módulos de interfaz de usuario y la posibilidad de un estilo parcial son mucho más significativas.
Aprovecha al máximo tus herramientas de peinado. No es dificil. Gracias por leer.