Estilo fuera de la caja

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





estilos iguales de lego
estilos iguales de lego

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



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 .





Izquierda - translúcido;  a la derecha - luz
- 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



, .





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.








All Articles