Di sini kami memiliki aplikasi. Serius, besar, dewasa. Hampir tidak ada gaya, tapi tidak ada kekacauan; kami menggunakan widget dari AppCompat untuk diri kami sendiri, tetapi telah memperketat topik dari Material Design Components (MDC) dan memikirkan tentang migrasi penuh.
Dan tiba-tiba ada tugas untuk mendesain ulang lengkap. Dan desain baru memiliki logika bisnis yang sama dengan yang lama. Komponennya baru, fontnya tidak standar, warnanya (kecuali untuk korporat) berbeda. Secara umum, realisasi datang bahwa sudah saatnya pindah ke MDC.
Tapi tidak semuanya sesederhana itu:
Desain ulang seharusnya dilakukan sedikit demi sedikit. Artinya, aplikasi akan berisi layar dengan tampilan lama dan tampilan baru.
Warna dan tipografi dalam desain baru berbeda dari yang direkomendasikan MDC. Meskipun prinsip penamaannya serupa
Lapisan presentasi dibagi menjadi modul ui yang terpisah. Dan beberapa di antaranya digunakan oleh aplikasi lain. Mempertimbangkan bahwa kita melakukannya tanpa gaya, untuk gaya dalam modul semacam itu, beberapa properti disembunyikan di balik atribut: warna, gaya teks, string, dan banyak lagi.
Ada skema yang ditetapkan tentang cara bekerja dengan modul ui di atas. Khususnya dengan atribut. Ini juga berarti dengan warna, gaya teks, string, dan lainnya. Dan dengan MDC, saya ingin menggunakan gaya
Selanjutnya, saya berbagi pengalaman saya tentang cara mengatasi kesulitan ini: bagaimana, ketika pindah ke MDC, untuk menyesuaikan sebagian aplikasi Android dengan modul ui independen, abstrak dari desain sistem dan tidak merusak apa pun. Bonus - saran dan analisis kesulitan yang saya temui.
Tentang modul ui
Ada modul ui. Mereka independen proyek. Berbaring terpisah darinya.
Ada modul root di dalam setiap proyek. Sebut saja presentasi inti . Itu tergantung pada modul ui yang digunakan dalam aplikasi ini. Modul dihubungkan sebagai ketergantungan gradle biasa.
Timbul pertanyaan. Bagaimana menata sesuatu? Singkatnya, menggunakan atribut. Dalam setiap modul ui tersebut, atribut yang digunakan ditentukan, yang harus diimplementasikan oleh tema aplikasi:
<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
, .
Jangan lupakan gaya. Padat adalah tentang gaya. Oleh karena itu, android:layout_height
dalam hal ini , harus terletak pada gaya. Dan ini buruk karena di setiap tempat penggunaan TextInputLayout
dengan gaya seperti itu, Anda harus memotong android:layout_height
dari markup ( jawaban atas pertanyaan mengapa demikian):
<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"
/>
Mungkin ini hanya bug dan di masa mendatang akan mungkin untuk menghindari solusi seperti itu.
Bagi saya, ini adalah solusi yang bagus. Ini memiliki kekurangan, tetapi keuntungan dalam bentuk abstraksi dari desain sistem dalam modul ui dan kemungkinan gaya parsial jauh lebih signifikan.
Manfaatkan alat penata gaya Anda sebaik mungkin. Itu tidak sulit. Terima kasih sudah membaca.