Styling outside the box

Here we have an application. Serious, big, adult. Virtually no styles, but no clutter; we use widgets from AppCompat for ourselves, but have already tightened the topic from Material Design Components (MDC) and are thinking about a full-fledged migration.





And suddenly there is a task for a complete redesign. And the new design has the same business logic with the old one. The components are new, the fonts are non-standard, the colors (except for the corporate ones) are different. In general, the realization comes that it's time to move to MDC.





But not everything is so simple:





  • Redesign is supposed to be piecemeal. That is, the application will contain both screens with the old and the new appearance.





  • The colors and typography in the new design are different from what the MDC recommends. Although the naming principles are similar





  • Presentation layer is split into separate ui modules. And some of them are used by another application. Considering that we do without styles, for styling in such modules, some properties are hidden behind attributes: colors, text styles, strings, and much more.





  • There is an established scheme for how to work with the above ui modules. In particular with attributes. This means also with colors, text styles, strings and more. And with MDC, I would like to use styles





Further, I share my experience of how to cope with these difficulties: how, when moving to MDC, to partially stylize an Android application with independent ui modules, abstract from the system design and not break anything at the same time. Bonus - advice and analysis of the difficulties that I encountered.





lego equal styles
lego equal styles

About ui modules

There are ui modules. They are project independent. Lie separately from him.





There is a root module inside each of the projects. Let's call it core-presentation . It depends on the ui modules that are used in this application. Modules are connected as a regular gradle dependency.





The question arises. How to stylize something? In short, using attributes. Within each such ui module, the used attributes are defined, which must be implemented by the application theme:





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





Left - translucent;  on the right - light
- 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



, .





Let's not forget about styles. Dense is about style. Therefore, android:layout_height



in this case , it must lie within the style. And this is bad because in each place of use TextInputLayout



with such a style you will have to cut android:layout_height



out from the markup (the answer to the question why this is so):





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



Perhaps this is just a bug and in the future it will be possible to avoid such workaround.






As for me, it turned out to be a good solution. It has its drawbacks, but the advantages in the form of abstraction from the system design in ui modules and the possibility of partial styling are much more significant.





Make the most of your styling tools. It's not hard. Thanks for reading.








All Articles