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