Ici, nous avons une application. Sérieux, grand, adulte. Pratiquement pas de styles, mais pas de désordre; nous utilisons des widgets d'AppCompat pour nous-mêmes, mais nous avons déjà resserré le sujet de Material Design Components (MDC) et réfléchissons à une migration à part entière.
Et tout à coup, il y a une tâche pour une refonte complète. Et le nouveau design a la même logique métier que l'ancien. Les composants sont nouveaux, les polices ne sont pas standard, les couleurs (sauf celles d'entreprise) sont différentes. En général, on se rend compte qu'il est temps de passer à MDC.
Mais tout n'est pas si simple:
La refonte est censée être fragmentaire. Autrement dit, l'application contiendra les deux écrans avec l'ancienne et la nouvelle apparence.
Les couleurs et la typographie du nouveau design sont différentes de ce que recommande le MDC. Bien que les principes de dénomination soient similaires
La couche de présentation est divisée en modules d'interface utilisateur distincts. Et certains d'entre eux sont utilisés par une autre application. Étant donné que nous nous passons des styles, pour le style dans de tels modules, certaines propriétés sont cachées derrière des attributs: couleurs, styles de texte, chaînes et bien plus encore.
Il existe un schéma établi pour savoir comment travailler avec les modules d'interface utilisateur ci-dessus. En particulier avec les attributs. Cela signifie également avec les couleurs, les styles de texte, les chaînes et plus encore. Et avec MDC, j'aimerais utiliser des styles
De plus, je partage mon expérience sur la façon de faire face à ces difficultés: comment, lors du passage à MDC, styliser partiellement une application Android avec des modules d'interface utilisateur indépendants, abstraire de la conception du système et ne rien casser. Bonus - conseils et analyse des difficultés que j'ai rencontrées.

À propos des modules d'interface utilisateur
Il existe des modules d'interface utilisateur. Ils sont indépendants du projet. Allongez-vous séparément de lui.
Il y a un module racine à l'intérieur de chacun des projets. Appelons cela la présentation de base . Cela dépend des modules d'interface utilisateur utilisés dans cette application. Les modules sont connectés comme une dépendance gradle régulière.

La question se pose. Comment styliser quelque chose? En bref, en utilisant des attributs. Dans chacun de ces modules d'interface utilisateur, les attributs utilisés sont définis, qui doivent être implémentés par le thème de l'application:
<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
View, . . , . .
, . 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
, .
N'oublions pas les styles. Dense est une question de style. Par conséquent, android:layout_height
dans ce cas , il doit s'inscrire dans le style. Et c'est mauvais car dans chaque lieu d'utilisation TextInputLayout
avec un tel style, vous devrez couper android:layout_height
du balisage (la réponse à la question pourquoi c'est):
<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"
/>
C'est peut-être juste un bogue et à l'avenir, il sera possible d'éviter une telle solution de contournement.
Quant à moi, cela s'est avéré être une bonne solution. Il a ses inconvénients, mais les avantages sous forme d'abstraction de la conception du système dans les modules d'interface utilisateur et la possibilité d'un style partiel sont beaucoup plus significatifs.
Tirez le meilleur parti de vos outils de coiffage. C'est pas difficile. Merci d'avoir lu.