Estilo fora da caixa

Aqui temos um aplicativo. Sério, grande, adulto. Praticamente sem estilos, mas sem desordem; usamos widgets do AppCompat para nós mesmos, mas já definimos o tópico dos Componentes de design de material (MDC) e estamos pensando em uma migração completa.





E de repente surge a tarefa de um redesenho completo. E o novo design tem a mesma lógica de negócios do antigo. Os componentes são novos, as fontes não são padronizadas, as cores (exceto as corporativas) são diferentes. Em geral, percebe-se que é hora de mudar para o MDC.





Mas nem tudo é tão simples:





  • O redesenho deve ser gradativo. Ou seja, o aplicativo conterá as duas telas com a aparência antiga e a nova.





  • As cores e tipografia no novo design são diferentes do que o MDC recomenda. Embora os princípios de nomenclatura sejam semelhantes





  • A camada de apresentação é dividida em módulos separados da interface do usuário. E alguns deles são usados ​​por outro aplicativo. Considerando que fazemos sem estilos, para estilizar esses módulos, algumas propriedades ficam ocultas por trás dos atributos: cores, estilos de texto, strings e muito mais.





  • Existe um esquema estabelecido de como trabalhar com os módulos de interface do usuário acima. Em particular com atributos. Isso também significa cores, estilos de texto, strings e muito mais. E com MDC, gostaria de usar estilos





Além disso, compartilho minha experiência de como lidar com essas dificuldades: como, ao mudar para MDC, estilizar parcialmente um aplicativo Android com módulos de interface do usuário independentes, abstrair do design do sistema e não quebrar nada. Bônus - conselho e análise das dificuldades que encontrei.





estilos iguais de lego
estilos iguais de lego

Sobre módulos de interface do usuário

Existem módulos de interface do usuário. Eles são independentes do projeto. Mentira separada dele.





Existe um módulo raiz dentro de cada um dos projetos. Vamos chamá-lo de apresentação do núcleo . Depende dos módulos de interface do usuário usados ​​neste aplicativo. Os módulos são conectados como uma dependência regular do gradle.





Surge a questão. Como estilizar algo? Resumindo, usando atributos. Dentro de cada um desses módulos de interface do usuário, são definidos os atributos usados, que devem ser implementados pelo tema do aplicativo:





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





Esquerda - translúcido;  à direita - 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



, .





Não vamos esquecer os estilos. Denso é sobre estilo. Portanto, android:layout_height



neste caso , deve estar dentro do estilo. E isso é ruim porque em cada local de uso TextInputLayout



com tal estilo, você terá que cortar android:layout_height



a marcação (a resposta para a pergunta por que isso é):





<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"
	/>
      
      



Talvez seja apenas um bug e, no futuro, será possível evitar essa solução alternativa.






Quanto a mim, acabou sendo uma boa solução. Ele tem suas desvantagens, mas as vantagens na forma de abstração do design do sistema em módulos de interface do usuário e a possibilidade de estilização parcial são muito mais significativas.





Aproveite ao máximo suas ferramentas de estilo. Não é díficil. Obrigado pela leitura.








All Articles