Material适配详解
这篇文章转载自GavinCT的:Material适配1 - 入门篇 与 Material适配2 - 高级篇 ,在这里两篇文章合并到了一起,内容思路清晰,浅显易懂。
随着Material Design的普及,很多开发人员都会面临App的Material适配。如果你的App不只是针对5.0以上设备的话(多数情况也必须做兼容), 那么下面的经验总结将会对你有所帮助。
当然,有些公司的App不会改成Material Design,但如果你以前使用AppCompatV7的话,升级到21后,你必然面临和以前不一样的使用方式,了解新的方式也是必须的。
言归正传,官方给我们的适配方案是AppCompatV7,新发布的22.1.1适配包相对于22又进行了较大的改动,同时对Material适配更加强大,因此本文主要介绍基于22.1.1版本的适配流程。
开始使用
compile 'com.android.support:appcompat-v7:22.1.1'
这里需要说明的是使用19、20及其以下版本仍然是Holo风格,
使用21和22版本都会有Material的效果。
Theme介绍
引用完库之后,首先要面对的是配置主题。否则如果你以前使用AppCompat的话,运行之后会发现App惨不忍睹。
分类
Theme主要有以下几种分类:
-
Theme.AppCompat (dark version)
-
Theme.AppCompat.Light (light version)
-
Theme.AppCompat.Light.DarkActionBar
如果以前使用ActionBar Holo风格时使用的就是AppCompat,那么这些地方是不需要更改的。
注: Material下的ActionBar会比之前更大,这点可在之后的ActionMode讨论中看到。
配置
Theme配置和原先有些不一样,配置示例如下:
<style name="Theme.App" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Main theme colors -->
<!-- your app branding color for the app bar -->
<item name="colorPrimary">@color/theme_primary</item>
<!-- darker variant for the status bar and contextual app bars -->
<item name="colorPrimaryDark">@color/theme_primary_dark</item>
<!-- theme UI controls like checkboxes and text fields -->
<item name="colorAccent">@color/theme_color_accent</item>
</style>
先上官方解释图:
图上的各参数都可以直接配置到主题中生效。其中colorPrimaryDark仅在Lollipop以上机器生效。
colorAccent解析
colorAccent会改变光标颜色、checkbox背景色等。
基本上可以理解为控件的主色调。
以Checkbox为例:
官方默认是绿色的
改变colorAccent为蓝色后
自定义Status Bar (Lollipop以上设备)
Material可以让你轻松订制Staus Bar。
-
可以使Theme中的
android:statusBarColor
属性来改变,默认从android:colorPrimaryDark
中获取。 -
代码设置:
Window.setStatusBarColor()
常见错误
现在AppCompat对窗口主题的flag要求更严格。
主要原因是为了支持Dialog,大量使用了 AppCompat 之前并没有重视的 windowNoTitle 标志。
升级到v22.1.0以后(包括本文讲述的22.1.1),你可能遇到下面的异常:
Caused by: java.lang.IllegalArgumentException: AppCompat does not support the current theme features
at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:360)
at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:246)
at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:106)
解决办法有两种:
-
最简单的是使用 Theme.AppCompat.NoActionBar 作为 parent theme,这样就会一直正常。
-
如果不能这样做(或许你需要同时支持ActionBar和NoActionBar,其实也可以通过第一种方式来解决,可能colorPrimary之类的需要多配置一遍),
你可以采用:
<style name="MyTheme" parent="Theme.AppCompat">
...
</style>
<style name="MyTheme.NoActionBar" parent="MyTheme">
<!-- Both of these are needed -->
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
AppCompatActivity使用
最新的22.1.1版本,ActionBarActivity已经废弃。开始采用AppCompatActivity。
如果你以前使用的是ActionBarActivity,建议替换掉,不需要更改其他代码。
(特别重要的AppCompatDelegate登场,具体代码可以查看AppCompatActivity实现,这里可以简单替换下快速适配,其实一般情况下也不需要自己来写AppCompatDelegate)
OK,到这里,其实你的App差不多就能正常运行了,只是有些细节方面还需要继续完善。
你可能已经注意到你的Dialog还不是Material风格,那么我们继续来看Dialog。
AppCompatDialog
AppCompat之前的21、22版本都没有实现Material Dialog。 在22.1.x发布时,这个问题终于解决了。
AppCompatDialog是AppCompat themed Dialog的 Base class.
目前他的子类只有AlertDialog,但已经足够使用。
使用方式也很简单,直接将AlertDialog改为android.support.v7.app
包下的AlertDialog即可。
其他使用方式一样,不需要做任何改动。
Preference
官方至今没有做到完全的适配。
对比图:
4.x设备上
5.x设备上
可以看到PreferenceCategory
和Preference
在4.x设备上底部都有横线,5.x设备上都没有。
也可以看到CheckBoxPreference是已经适配了的。
为了能让Preference适配的更加彻底,推荐下常用的第三方适配库: Android-MaterialPreference
但是作者并没有去写DialogPreference一类的,比如常见的ListPreference。
其实这里是有解决办法的。上面已经写到了新版的AlertDialog,配合How can I change the appearance of ListPreference Dialog 这篇帖子,就不难实现。
但也可以看到有人讨论了Material规范中提到的实现方式,当然也有人根据Google规范进行了实现,这里可以根据需求来自行选择实现方式。
关于Preference需要说明的是:
-
如果app是针对11以上的,推荐使用AppCompatActivity和PreferenceFragment来实现。
-
如果兼容更早的版本,需要借助AppCompatDelegate来实现,Google的示例代码:AppCompatPreferenceActivity.java
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.supportv7.app;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
* to be used with AppCompat.
*
* This technique can be used with an {@link android.app.Activity} class, not just
* {@link android.preference.PreferenceActivity}.
*/
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
public ActionBar getSupportActionBar() {
return getDelegate().getSupportActionBar();
}
public void setSupportActionBar(@Nullable Toolbar toolbar) {
getDelegate().setSupportActionBar(toolbar);
}
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}
Material中还有一个重要的特性是阴影的设置和波纹效果的实现,这里来粗略说明一下:
Elevation - 设置阴影
v21以后在View的xml中使用android:elevation
属性,或者在代码中使用View的setElevation()
方法。
兼容老版的阴影策略
还是需要使用.9图片的阴影来做。
注: ViewCompat.setElevation() sadly doesn't apply shadows in pre-Lollipop.
RippleDrawable - 波纹效果
使用已经提供好的
-
?android:attr/selectableItemBackground
扩散到View边界 -
?android:attr/selectableItemBackgroundBorderless
设置后,会从孩子往父亲找一个依附的色。如果View往上找的时候,亲生父亲没背景色,会继续向上查找直到最顶端。找到了最顶端的爷爷,这个时候才绘制。
然而,如果父亲的兄弟又绘制了颜色,且盖住了最顶端的绘制,会导致看不到效果。如果有一定的透明度,结果就显而易见了。
特别注意: -
当把硬件加速给关闭时,这个效果是没有的。
-
这是API 21的新属性,老版本无法使用.
改变默认响应色
改变Theme中的android:colorControlHighlight
属性。
自定义
<!-- A green ripple drawn atop a black rectangle. -->
<ripple android:color="#ff00ff00">
<item android:drawable="@android:color/black" />
</ripple>
<!-- A blue ripple drawn atop a drawable resource. -->
<ripple android:color="#ff0000ff">
<item android:drawable="@drawable/my_drawable" />
</ripple>
android:color
中是点击响应色,也是波纹扩散色。
item中是正常状态下的显示。
一般使用时会和原有的selector配合,原有的selector负责5.0以下显示效果,
新的selector内部含有ripple标签放在drawable-v21中,保证点击效果。
写到这里,我觉得对一个中国开发者的Material入门篇来说,还需要说明下魅族适配的问题
关于魅族SmartBar适配问题
和魅族官方技术人员沟通过,不(pu)幸(tian)被(tong)告(qing)知(a):使用AppCompatV7 21以上,暂时无法进行SmartBar适配。
原因大概解释如下:
-
v19的时候,ActionBar的处理是:如果系统有,系统处理;系统没有,自己画。
-
v21以后都是Compat库自己画了,不会调用系统的,因此魅族无法获取合并到SmartBar中。
so,坐等魅族找到新的适配策略或者放弃SmartBar~~
ActionBar --> Toolbar
在使用ActionBar的时候,一堆的问题:这个文字能不能定制,位置能不能改变,图标的间距怎么控制神马的,由此暴露出了ActionBar设计的不灵活。
在上一篇中,我们只是简单使用了AppCompatActivity,他使用的仍然是ActionBar
官方在21以后提供了ToolBar。
Toolbar之所以灵活,是因为它其实就是一个ViewGroup,我们在使用的时候和普通的组件一样,在布局文件中声明。
主题使用
使用Toolbar时,如果单纯的当作控件来使用,主题是不需要单独设置的。
但是如果想用他来替代ActionBar,
那么需要配置为Theme.AppCompat.NoActionBar
主题,
或者在主题中加入
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
(两个都必须有,上一篇已经提到没有windowNoTitle
时会报错)
这里推荐使用第一种方式。
常用的配置
Toolbar因为经常被用来替代ActionBar,所以一般项目里都会抽取出来,以便include。
可能有人会说,既然还是用来替换ActionBar,那我项目里直接不动ActionBar不就完了?
对,一般情况下是没有问题的,但是有些界面需要ActionMode的时候,你就被迫要换成Toolbar了。
这是后话,我们这篇会谈到。
先来看Toolbar常用代码:
include_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:minHeight="?attr/actionBarSize" />
配置中需要注意的是theme和popupTheme,我们来仔细看下,先从View的theme说起。
View的theme
Android 5.0引入一个全新的特性,允许你对view设置theme,这种设置会影响控件及其包含的子控件。
使用AppCompat v22.1.x 后,也可以给你 layout 里的任意视图设置主题。
只要使用 android:theme
这个属性就好,新版本的兼容库可以在 compat 和 framework 之间无缝地切换功能。
实现原理
这是因为有ContextThemeWrapper
类,这个类API v1的时候就有了。
他包裹(wrap)一个存在的Context(这里指你的Activity),之后覆盖(overlay)一个新的主题在当前Context的主题之上,这也是为什么叫ThemeOverlay。
Toolbar常用的ThemeOverlay
-
ThemeOverlay.AppCompat.Light.ActionBar
-
ThemeOverlay.AppCompat.Dark.ActionBar
android:theme 与 app:theme
在AppCompat v21里,提供了一个快速方便的方法设置Toolbar的主题,使用app:theme。
而新版本22.1.x中,AppCompat 允许对 Toolbar 使用android:theme
代替app:theme
。
最好的一点是:它会自动继承父视图的theme ,并且兼容所有APIv11以上的设备。
示例:
<Toolbar
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<!-- This TextView inherits its theme from the parent Toolbar -->
<TextView android:text="I'm light!" />
</Toolbar>
对于运行 API v10 甚至更老的设备来说,你也可以使用android:theme
属性, 不过它不会继承父视图theme。
这就意味着你要么重新考虑你的布局,要么为每一个子视图都设置上 android:theme 属性。(这样做效率真的很低)
总结一下:
-
兼容 API 11 以上,推荐使用
android:theme
-
如果兼容更老的版本,推荐继续使用
app:theme
app:popupTheme
有时候我们有需求:
ActionBar文字是白的,ActionBar Overflow弹出的是白底黑字
让ActionBar文字是白的,那么对应的theme肯定是Dark。
可是让ActionBar弹出的是白底黑字,那么需要Light主题。
这时候popupTheme就派上用场了。
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:minHeight="?attr/actionBarSize" />
注意:
使用app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
而不是android:popupTheme
作为ActionBar使用
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.blah);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
独立使用
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.blah);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
// Set an OnMenuItemClickListener to handle menu item clicks
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
// Handle the menu item
return true;
}
});
// Inflate a menu to be displayed in the toolbar
toolbar.inflateMenu(R.menu.your_toolbar_menu);
}
一般使用疑问
1. 没有了splitActionBarWhenNarrow,用两个Toolbar模拟是否可以?
不可以,这种方式是有问题的。
两个Toolbar放在布局中后,下面的Toolbar不能顶到最左边。
stackoverflow : How to center action menu on toolbar 中有详细的描述。
问题中给出了SplitToolbar
的解决方案,但我尝试后发现这种解决方案仍然有轻微的偏移。
2. 使用Toolbar后,NavigationIcon不垂直居中?
Toolbar的layout_height属性,要用“?attr/actionBarSize”而不是“?android:attr/actionBarSize”,替换后可解决NavigationIcon不垂直居中的问题。
原因是系统的actionBarSize比AppCompat中的要小。使用“?android:attr/actionBarSize”调用了较小的那个。
ActionMode配置
以前ActionMode都是从Activity启动,使用Material主题后应该使用toolbar启动。(相应的,这个界面需要改写成NoActionBar,然后使用Toolbar)
代码示例:
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.startActionMode(mActionModeCallback)
同时需要在主题中配置
<item name="windowActionModeOverlay">true</item>
ActionMode背景色替换
<!--action Mode背景-->
<item name="actionModeBackground">@color/theme_color_action_mode</item>
如何实现和Inbox一样的ActionMode
可以看到,ActionMode开启时,顶部的Status Bar颜色也跟着改变了
这种功能Theme中并没有提供属性来修改。
但是联想到入门篇提到的代码设置status bar颜色,这里就不难实现了。
代码共享下:
private int mOldStatusBarColor = -1;
private void setActionModeStatusBarColor(int colorResId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mOldStatusBarColor = mActivity.getWindow().getStatusBarColor();
setStatusBarColorCore(mActivity.getResources().getColor(colorResId));
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setStatusBarColorCore(int color) {
mActivity.getWindow().setStatusBarColor(color);
}
private void resetStatusBarColor() {
if (mOldStatusBarColor != -1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
setStatusBarColorCore(mOldStatusBarColor);
mOldStatusBarColor = -1;
}
}
开启时调用set,销毁时调用reset即可。
P.S. : 上面的计数可以通过setTitle来完成。
网上提供的错误方式(已踩坑,请绕行)
保持Activity调起,使用android:windowActionModeOverlay
属性。
看似让ActionMode浮在了ActionBar上,但其实存在很大问题。
这种方式在4.4以下会使用Holo风格(overflow图标可以看出来,不是三个原点,是三个方块),且ActionMode比ActionBar小一些(可以看到蓝色底边是ActionBar)