Un style hors des sentiers battus

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.





styles égaux lego
styles égaux lego

À 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 .





Gauche - translucide;  à droite - lumière
- translucent; - light

, 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.








All Articles