客户端开发中UI设计极其重要,直接影响用户体验和App的品质;其次UI设计应做到样式、排版统一,简化布局文件,方便全局修改和维护。
一、样式排版统一
1.1 共用style
基础颜色表
在values资源文件夹下添加文件colors.xml,加入常用的基础颜色值,使全局组件色调保持一致:
除基础颜色,还可添加App主题色调,使得ActionBar、Tab等组件颜色和主题色保持一致:
统一布局尺寸和文字大小
Android界面设计需要统一排版,如图标边距、文字大小、ListItem间隔等,在values资源文件夹下添加文件dimen.xml,添加统一的布局距离和文字大小:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="font_larger">22sp</dimen>
<dimen name="font_large">18sp</dimen>
<dimen name="font_normal">16sp</dimen>
<dimen name="font_small">14sp</dimen>
<dimen name="font_smaller">12sp</dimen>
<dimen name="font_smallest">10sp</dimen>
<dimen name="spacing_huge">40dp</dimen>
<dimen name="spacing_larger">34dp</dimen>
<dimen name="spacing_large">24dp</dimen>
<dimen name="spacing_biger">20dp</dimen>
<dimen name="spacing_big">18dp</dimen>
<dimen name="spacing_normal">14dp</dimen>
<dimen name="spacing_small">12dp</dimen>
<dimen name="spacing_smaller">10dp</dimen>
<dimen name="spacing_smallest">8dp</dimen>
<dimen name="spacing_tiny">6dp</dimen>
<dimen name="spacing_tinyer">4dp</dimen>
<dimen name="spacing_tinyest">2dp</dimen>
<dimen name="spacing_border">12dp</dimen>
</resources>
界面排版等的尺寸可以参考如下布局,
- 菜单选项内边距、字体颜色、选中颜色、背景色、上线分割线
- ListView中Item的外边距、图标尺寸、图标和内容的间距、内容区标题和内容的文字尺寸颜色、Item分割线
- Tab菜单选项图标尺寸、文字尺寸、Item间隔、Item选中样式
统一样式
应用内组件的样式应保持统一,比如按钮、弹窗、菜单列表等,在values资源文件夹下定义styles.xml(或新建文件把样式分离出来,如style-btn.xml),方便全局修改。
如下在布局文件中添加几个按钮,无任何样式:
<Button
android:id="@+id/main_module_mine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Mine" />
<Button
android:id="@+id/main_module_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Message" />
<Button
android:id="@+id/main_module_theme"
style="@style/ButtonTheme"
android:text="Theme" />
现加入按钮字体、内边距、背景等样式,
<Button
android:id="@+id/main_module_mine"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/theme_button_selector"
android:paddingBottom="@dimen/spacing_smallest"
android:paddingTop="@dimen/spacing_smallest"
android:text="Mine"
android:textColor="@color/white"
android:textSize="@dimen/font_normal" />
...
...
theme-button-selector.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="3dip" />
<stroke android:width="1dip" android:color="@color/colorPrimary" />
<gradient android:angle="-90" android:endColor="@color/colorPrimary" android:startColor="@color/colorPrimary" />
</shape>
</item>
<item android:state_focused="true">
<shape android:shape="rectangle">
<corners android:radius="3dip" />
<stroke android:width="1dip" android:color="@color/colorPrimary" />
<solid android:color="@color/colorPrimaryDark" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="3dip" />
<stroke android:width="1dip" android:color="@color/colorPrimary" />
<gradient android:angle="-90" android:endColor="@color/colorPrimary" android:startColor="@color/colorPrimary" />
</shape>
</item>
</selector>
加入统一的样式后,三个按钮好看些了-:
但布局文件也变得格外冗长,为减少重复的布局代码,抽离通用样式,在styles.xml添加如下元素:
<style name="ButtonTheme" parent="@android:style/Widget.Button">
<item name="android:textSize">@dimen/font_normal</item>
<item name="android:textColor">@color/white</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_margin">@dimen/spacing_tiny</item>
<item name="android:paddingTop">@dimen/spacing_smallest</item>
<item name="android:paddingBottom">@dimen/spacing_smallest</item>
<item name="android:background">@drawable/theme_button_selector</item>
</style>
重新修改布局文件,三个按钮使用通用样式,代码简化了很多:
<Button
android:id="@+id/main_module_mine"
style="@style/ButtonTheme"
android:text="Mine" />
<Button
android:id="@+id/main_module_message"
style="@style/ButtonTheme"
android:text="Message" />
<Button
android:id="@+id/main_module_theme"
style="@style/ButtonTheme"
android:text="Theme" />
布局重用
有些布局组件可在全局复用,例如自定义TitleBar、ActionBar,本项目Modulize使用第三方库CommonTitleBar作为标题栏布局,在layout资源文件夹中定义common_titlebar.xml:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<com.wuhenzhizao.titlebar.widget.CommonTitleBar xmlns:titlebar="http://schemas.android.com/apk/res-auto"
android:id="@+id/titlebar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
titlebar:centerTextColor="@color/white"
titlebar:centerTextSize="@dimen/font_normal"
titlebar:centerType="textView"
titlebar:fillStatusBar="true"
titlebar:leftImageResource="@drawable/common_transparent"
titlebar:leftType="imageButton"
titlebar:rightType="imageButton"
titlebar:showBottomLine="false"
titlebar:statusBarColor="?attr/colorPrimaryDark"
titlebar:titleBarColor="?attr/colorPrimary" />
</merge>
在activity布局文件中使用include引入此布局,merge标签为了减少视图层级(详细使用参考Android抽象布局):
<include layout="@layout/common_titlebar" />
<Button
android:id="@+id/main_module_mine"
style="?android:attr/buttonStyle"
android:text="Mine" />
布局复用可以有效地统一标题栏风格,每个页面设置不同的标题和图标:
commonTitleBar = findViewById(R.id.common_titlebar);
commonTitleBar.getCenterTextView().setText("标题栏");
commonTitleBar.getRightImageButton().setImageResource(R.drawable.main_action_icon_user);
1.2 UI模块lib-ui
模块化开发应用模块之间不直接相互依赖,各模块之间内的样式不可直接被其他模块调用,因此有必要创建UI基础库,将公共样式放在UI库中。
按照Android组件化-基础框架搭建中基础库搭建方法,新建lib-ui存放公共样式和资源文件:
├─res
| ├─values
| | ├─colors.xml
| | ├─dimens.xml
| | ├─strings.xml
| | ├─styles.xml
| | └theme.xml
| ├─layout
| | └common_titlebar.xml
| ├─drawable-xxxhdpi
| | ├─action_bar_add.png
| ├─drawable-xxhdpi
| | ├─action_bar_add.png
| ├─drawable-xhdpi
| | ├─action_bar_add.png
| ├─drawable-mdpi
| | ├─action_bar_add.png
| ├─drawable-hdpi
| | ├─action_bar_add.png
| ├─drawable
| | ├─common_transparent.xml
| | └theme_button_selector.xml
使lib-common依赖lib-ui,因此各应用模块就可以使用lib-ui中的公共样式。
二、主题切换
主题切换功能开发思路如下:
- 根据上述布局风格统一原则配置两套主题
- 在Activity中为App设置主题
- 动态设置主题,主题设置立即生效
- 复杂的View组件随主题动态变化
2.1 主题配置
配置至少两个主题
在lib-ui\src\main\res下添加两个资源文件theme-default.xml、theme-dark.xml,
├─values
| ├─theme-dark.xml
| ├─theme-default.xml
| └theme.xml
在theme.xml添加主题父类,theme-default和theme-dark中分别定义两个主题继承theme中的父主题:
theme.xml:
<!-- Base application theme. -->
<style name="AppBaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!--<item name="android:background">@drawable/main_background</item>-->
<item name="android:windowNoTitle">true</item>
</style>
theme-default.xml:
<!-- Default application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@color/light_gray</item>
<item name="android:buttonStyle">@style/ButtonTheme</item>
</style>
<color name="colorPrimary">#289ff4</color>
<color name="colorPrimaryDark">#0b79b7</color>
<color name="colorAccent">@color/white</color>
<style name="ButtonTheme" parent="@android:style/Widget.Button">
<item name="android:textSize">@dimen/font_normal</item>
<item name="android:textColor">@color/white</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_margin">@dimen/spacing_tiny</item>
<item name="android:paddingTop">@dimen/spacing_smallest</item>
<item name="android:paddingBottom">@dimen/spacing_smallest</item>
<item name="android:background">@drawable/theme_button_selector</item>
</style>
theme-dark.xml:
<!-- Dark application theme. -->
<style name="AppDarkTheme" parent="AppBaseTheme">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorDarkPrimary</item>
<item name="colorPrimaryDark">@color/colorDarkPrimaryDark</item>
<item name="colorAccent">@color/colorDarkAccent</item>
<item name="android:windowBackground">@color/colorDarkPrimary</item>
<item name="android:buttonStyle">@style/DarkButtonTheme</item>
</style>
<color name="colorDarkPrimary">#222222</color>
<color name="colorDarkPrimaryDark">#333333</color>
<color name="colorDarkAccent">#333333</color>
<style name="DarkButtonTheme" parent="@android:style/Widget.Button">
<item name="android:textSize">@dimen/font_normal</item>
<item name="android:textColor">@color/white</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_margin">@dimen/spacing_tiny</item>
<item name="android:paddingTop">@dimen/spacing_smallest</item>
<item name="android:paddingBottom">@dimen/spacing_smallest</item>
<item name="android:background">@drawable/theme_button_selector</item>
</style>
配置的内容
主题配置中重要的配置项,参见Material Design的The Color System:
- colorPrimary:基色,跨域整个App各个页面和组件最常用的颜色,常用于应用栏(Appbar)
- colorPrimaryDark:重基色,一般为状态栏(Sytembar)的颜色,与应用栏形成对比色
- colorAccent:着重色,各View被选中或突出显示时的颜色;Item或CardView的背景色
- android:windowBackground:界面背景色
- android:buttonStyle:按钮样式;其他组件样式也可全局定义
各样式和value在activity布局文件中使用如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground"
android:orientation="vertical"
tools:context="org.blackist.modulize.main.view.MainActivity">
<include layout="@layout/common_titlebar" />
<Button
android:id="@+id/main_module_mine"
style="?android:attr/buttonStyle"
android:text="Mine" />
<Button
android:id="@+id/main_module_message"
style="?android:attr/buttonStyle"
android:text="Message" />
<Button
android:id="@+id/main_module_theme"
style="?android:attr/buttonStyle"
android:text="Theme" />
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_tiny"
android:background="?attr/colorAccent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/spacing_tiny"
android:background="?attr/colorAccent"
android:text="Use colorAccent \nAs \nItem Backgroud" />
</android.support.v7.widget.CardView>
</LinearLayout>
为页面设置背景色,使用 background="?android:windowBackground" 属性;
colorAccent用作List Item布局 或 局部布局的背景,当主题切换时Item背景随之切换,使用方式 background="?attr/colorAccent";
Button等组件的样式使用 **style="?android:attr/buttonStyle"**设置;
本项目文字颜色自适应,即根据当前主题,安卓系统会自动设置字黑色或白色;
从 ?android:windowBackground 和 ?colorAccent 中可以看出,根据如下主题配置项配置方式,决定布局文件中使用这些属性的方式:
2.2 主题切换
使用SDK中的setTheme方法设置主题,设置主题需要在setContentView()之前调用:
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// before set ContentView
setTheme(mThemeDefault ? R.style.setTheme : R.style.AppTheme);
setContentView(R.layout.main_activity);
}
mThemeDefault为boolean类型的值,存储在SharedPreference中,App启动时读取其值使得App记住用户偏好。
切换后的主题如下:
2.3 主题动态切换
当使用按钮或Switch触发主题设置后,视图已经创建,设置不能立即生效,需要重启App才能看到效果。想要立即生效则需要重建当前栈中所有activity,因此需要获取到所有已加载activity,使用lib-apptools下的AppManager工具类,在Activity的onCreate()中将自身加入Activity栈:
AppManager.getInstance().addActivity(this);
在onDestory()中使activity出栈:
AppManager.getInstance().removeActivity(this);
调用AppManager.getInstance().recreateAllActivity()方法重建栈中Activity,使得主题切换立即生效。
三、组件主题
配置某些组件跟随主题变换颜色等样式。
3.1 AlertDialog
配置Dialog的默认样式类似于Button的全局样式,但稍加复杂一些。
在theme-default.xml中:
<!-- Default application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<item name="alertDialogTheme">@style/AlertDialog</item>
</style>
<style name="AlertDialog" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:windowTitleStyle">@style/AlertDialogTitle</item>
<item name="colorAccent">@color/colorPrimary</item>
<item name="android:background">@color/colorAccent</item>
</style>
<style name="AlertDialogTitle">
<item name="android:textAppearance">@style/AlertDialogTitleStyle</item>
</style>
<style name="AlertDialogTitleStyle" parent="@android:style/TextAppearance.Holo.DialogWindowTitle">
<item name="android:textSize">@dimen/font_normal</item>
<item name="android:textColor">@color/colorPrimary</item>
</style>
theme-dark.xml
<!-- Dark application theme. -->
<style name="AppDarkTheme" parent="AppBaseTheme">
<item name="alertDialogTheme">@style/DarkAlertDialog</item>
</style>
<style name="DarkAlertDialog" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:windowTitleStyle">@style/DarkAlertDialogTitle</item>
<item name="colorAccent">@color/text_hint</item>
<item name="android:background">@color/colorDarkAccent</item>
</style>
<style name="DarkAlertDialogTitle">
<item name="android:textAppearance">@style/DarkAlertDialogTitleStyle</item>
</style>
<style name="DarkAlertDialogTitleStyle" parent="@android:style/TextAppearance.Holo.DialogWindowTitle">
<item name="android:textSize">@dimen/font_normal</item>
<item name="android:textColor">@color/text_hint</item>
</style>
在Activity中new AlertDialog即可,无需多余的样式设置:
mTypeDialog = new AlertDialog.Builder(MainActivity.this)
.setIcon(R.mipmap.ic_launcher_round)
.setTitle("AlertDialog Theme")
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setPositiveButton("Confirm", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).create();
mTypeDialog.show();
切换主题后,AlertDialog样式随之变化:
3.2 获取当前主题属性
在某些自定义组件中需要获取App主题色,比如在AlertDialog中添加一个轮滑选择器,自定义组件Whiew(在lib-ui\src\main\java\org\blackist\modulize\ui\widget\whiew下),当设置文本时需要获取当前主题的相关属性来设置样式。
获取Color
TypedValue typedValue = new TypedValue();
Theme theme = context.getTheme();
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
@ColorInt int color = typedValue.data;
...
获取Dimen
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimensionPixelSize(R.dimen.font_normal));
项目Github地址:https://github.com/blackist/modulize