Material Design 控件之 Toolbar 非完全解析

一、Toolbar 出现的背景

Toolbar 是 Android5.0 中新引入的一个控件,其出现的目的就是为了取代 ActionBar。
Actionbar 在 Android3.0 推出的目的就是为了在 UI 界面中引入一个全局导航的功能,取代 Android3.0 之前的标题栏。并且在刚推出不久就发布了兼容到 API 7 的兼容包,但是很多应用都没有使用 Actionbar。
原因是 Actionbar 的界定很模糊,因为 Android 系统中把界面分成两大部分,一部分是 System UI,主要由 Statusbar 和 Android4.0 以后出现的由虚拟按键构成的导航栏为主,对于系统 UI 来说,Android 不允许应用开发者对其进行完全控制,在 Android5.0 之后甚至不让开发者控制状态栏;另一部分就是应用 UI,对于这部分 UI,开发者拥有完全控制权。
而 Actionbar 在显示上应该算是应用 UI 的一部分,但是开发者又不能对其进行完全控制,因为它毕竟是由系统创建并对其进行相关参数的初始化。所以在实际开发中,很多开发者都是用布局生成一个模拟的 Actionbar 来代替系统的 Actionbar,基于这一点,Android 在5.0后推出一个新的控件 Toolbar 来取代 ActionBar。

二、Toolbar 的使用

Toolbar

1)导包

Toolbar 位于 android.widget 包下,在 Android5.0 + 版本可以直接使用,如果需要兼容 Android5.0 以下的版本,那么需要使用位于支持包 android.support.v7.widget 中的 Toobar,可以支持到 API7。
直接在 Module 的 build.gradle 文件中添加依赖

dependencies {
    compile 'com.android.support:appcompat-v7:25.3.1'
}

2)设置 Theme

要想使用 Toolbar 代替 Actionbar,必须使用 Theme.AppCompat 主题中没有 Actionbar 的主题,把 Actionbar 隐藏掉,否则会造成冲突,NoActionbar 的主题对两个 Window 属性进行了重写:

<style name="Theme.AppCompat.Light.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
</style>

经过实际测试,发现其实只需要设置 <item name="windowNoTitle">true</item> 即可

3)xml 文件中添加

不同于 Actionbar ,系统会自动根据设置的 Theme 创建不同样式的 Actionbar 添加到界面中,Toolbar 需要像普通控件一样在布局文件中添加:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"/>
</LinearLayout>

但是运行之后会发现界面中 Actionbar 位置只有一个 Toobar 的android:background 属性所指定颜色的背景,其它的什么都没有,其实这个时候 Toolbar 已经出现了,只是因为我们除了 background 外没有给 Toolbar 设置任何属性,ActionBar 的属性是系统自动初始化的,Toolbar 需要我们进行设置。
给 Toolbar 设置属性有三种方式,可以结合使用:

1、调用 setSupportActionbar/setActionbar 方法和 ·getSupportActionbar/getActionbar 把 Toolbar 作为 Actionbar 处理
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }
}

通过 findViewById 方法获取到 xml 文件中添加的 Toolbar 控件,然后调用 setSupportActionbar 方法,如果只支持 5.0 以上系统调用 setActionbar 方法,这个方法是把 Toolbar 当作 Actionbar 来处理,把系统给 Actionbar 设置的相关初始参数设置给 Toolbar,之前关于 Actionbar 的大部分操作都会应用在 Toolbar 上。
setSupportActionbar 是 AppCompatActivity 中的方法,所以使用这种方式的 Activity 必须继承 AppCompatActivity。
这里的参数是 Toolbar 而并非 Actionbar,简单看一下源码:

AppCompatActivity#setSupportActionbar
public void setSupportActionBar(@Nullable Toolbar toolbar) {
    getDelegate().setSupportActionBar(toolbar);
}

只是简单地调用了 AppCompatDelegate 的 setSupportActionbar 方法:

AppCompatDelegateImplV7#setSupportActionbar

AppCompatDelagate 类是个抽象类,它的实现类 AppCompatDelegateImplV7 实现了 setSupportActionbar 方法:

@Override
public void setSupportActionBar(Toolbar toolbar) {
    if (!(mOriginalWindowCallback instanceof Activity)) {
        return;
    }

    final ActionBar ab = getSupportActionBar();
    if (ab instanceof WindowDecorActionBar) {
        throw new IllegalStateException("This Activity already has an action bar supplied " +
                "by the window decor. Do not request Window.FEATURE_ACTION_BAR and set " +
                "windowActionBar to false in your theme to use a Toolbar instead.");
    }

    ToolbarActionBar tbab = new ToolbarActionBar(toolbar, ((Activity) mContext).getTitle(),
            mAppCompatWindowCallback);
    setSupportActionBar(tbab);
    mWindow.setCallback(tbab.getWrappedWindowCallback());
    tbab.invalidateOptionsMenu();
}

这段方法的逻辑非常简单,首先会判断当前的窗口回调是否是一个
Activity 对象,因为只有 Activity 才能够支持创建 ActionBar,不是的话就返回,然后就会尝试去获取当前的 ActionBar,如果发现当前 Activity 中已经有了 ActionBar 就会抛出一个异常,这就是我们要设置 Toolbar 来替代 ActionBar 那么就必须去掉原有的
ActionBar 的原因,当然如果只是将 Toolbar 作为一个普通的控件,就不是必须的了。
然后将传入的 Toolbar 对象作为入参构造了一个 ToolbarActionBar 对象,随后马上调用了 setSupportActionBar 方法并将这个
ToolbarActionBar 对象传入,来看 ToolbarActionBar 类:

ToolbarActionBar
public class ToolbarActionBar extends ActionBar {
    // ......省略一些代码......

    public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback callback) {
        mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false);
        mWindowCallback = new ToolbarCallbackWrapper(callback);
        mDecorToolbar.setWindowCallback(mWindowCallback);
        toolbar.setOnMenuItemClickListener(mMenuClicker);
        mDecorToolbar.setWindowTitle(title);
    }

    // ......省略大量代码......
}

在 ToolbarActionBar 的构造方法中,又将 toolbar 作为入参构造了一个 ToolbarWidgetWrapper 对象,从类名里就可以看出,这个
ToolbarWidgetWrapper 是个包装类,它持有 Toolbar 的引用,其实用心观察 ToolbarActionBar 这个类你会发现他是一个代理类,其中大部分方法都是间接由 mDecorToolbar 这个对象调用,mDecorToolbar 对象的实际类型就是刚才我们所说的
ToolbarWidgetWrapper,而引用类型则是 DecorToolbar,ToolbarActionBar 这个类以委托代理的方式将自身的功能交由
mDecorToolbar 实现:

public class ToolbarActionBar extends ActionBar {
    // ......省略一些代码......

    @Override
    public void setIcon(int resId) {
        mDecorToolbar.setIcon(resId);
    }

    @Override
    public void setIcon(Drawable icon) {
        mDecorToolbar.setIcon(icon);
    }

    @Override
    public void setLogo(int resId) {
        mDecorToolbar.setLogo(resId);
    }

    @Override
    public void setLogo(Drawable logo) {
        mDecorToolbar.setLogo(logo);
    }

    // ......省略一些代码......
}

mDecorToolbar 的方法中对持有的 Toolbar 成员变量进行相关设置。
ToolbarActionBar 里面有一些方法是空的,那么 Actionbar 的相关属性就没有被关联到 Toolbar 中,其中就包括 android:background'属性,如果我们不给 Toolbar 设置该属性的话, Toolbar 就会默认显示 Window 窗口的背景色,即 Activity 所使用的主题中 android:windowBackground 属性指定的颜色。

也可以调用 getSupportActionbar 方法获取被当做 Actionbar 处理的 Toolbar 对象,并调用 Actionbar 的相关方法对 Toolbar 进行操作,这个时候 Toolbar 就是一个 Actionbar。

2、调用 Toolbar 的相关方法

Toolbar 自身也对外提供了一系列的方法对 Toolbar 进行相关设置,这个时候不需要调用 setSupportActionbar 方法,Activity 也就不必非要继承 AppCompatActivity,而且即使不隐藏 Actionbar,也不会造成冲突报错,但是 Actionbar 会占用 Toolbar 的位置,将 Toolbar 挤到下面去,所以作为导航栏使用的话,一般还是要隐藏掉 Actionbar。

//设置导航图标
setNavigationIcon(@DrawableRes int resId)
setNavigationIcon(@Nullable Drawable icon)
setNavigationOnClickListener(OnClickListener listener)
//设置 Logo
setLogo(@DrawableRes int resId)
setLogo(Drawable drawable)
//设置标题
setTitle(@StringRes int resId)
setTitle(CharSequence title)
//设置标题文本颜色
setTitleTextColor(@ColorInt int color)
//设置标题外观,包括字体颜色、大小、样式等
setTitleTextAppearance(Context context, @StyleRes int resId)
//设置标题边距像素
setTitleMargin(int start, int top, int end, int bottom)
setTitleMarginStart(int margin)
setTitleMarginTop(int margin)
setTitleMarginEnd(int margin)
setTitleMarginBottom(int margin)
//设置副标题
setSubtitle(@StringRes int resId)
setSubtitle(CharSequence subtitle)
//设置副标题文本颜色
setSubtitleTextColor(@ColorInt int color)
//设置副标题外观,包括字体颜色、大小、样式等
setSubtitleTextAppearance(Context context, @StyleRes int resId)
//加载菜单
inflateMenu(@MenuRes int resId)
//监听菜单点击
setOnMenuItemClickListener(OnMenuItemClickListener listener)
//设置弹出菜单主题
setPopupTheme(@StyleRes int resId)
//设置溢出菜单图标
setOverflowIcon(@Nullable Drawable icon)
3、在 xml 文件中设置属性

就像普通控件一样,需要注意的是一些属性的命名空间并不是 'android',需要在根布局声明自定义的命名空间,比如这里使用 app

xmlns:app="http://schemas.android.com/apk/res-auto"

这里的属性是很多的:

        //应用图标、导航图标、收缩图标
        app:logo=""
        app:logoDescription=""
        app:navigationIcon=""
        app:navigationContentDescription=""
        app:collapseIcon=""
        app:collapseContentDescription=""
        //样式
        app:theme=""
        //弹窗样式
        app:popupTheme=""
        //标题样式
        app:title=""
        app:titleTextAppearance=""
        app:titleTextColor=""
        app:titleMargin=""
        app:titleMargins=""
        app:titleMarginStart=""
        app:titleMarginTop=""
        app:titleMarginBottom=""
        app:titleMarginEnd=""
        //副标题样式
        app:subtitle=""
        app:subtitleTextAppearance=""
        app:subtitleTextColor=""
        //按钮
        app:buttonGravity=""
        app:maxButtonHeight=""
        //一些边距
         app:contentInsetEnd=""
        app:contentInsetLeft=""
        app:contentInsetRight=""
        app:contentInsetStart=""
        app:contentInsetEndWithActions=""
        app:contentInsetStartWithNavigation=""
        app:paddingEnd=""
        app:paddingStart=""
app:popupTheme 和 app:theme

app:popupTheme 用来设置溢出菜单的样式,通常继承一个 ThemeOverlay.AppCompat 主题,当然并不是必须的,然后重写相关属性:

<style name="PopupTheme" parent="ThemeOverlay.AppCompat.Light">
        <item name="android:colorBackground">@color/colorAccent</item>
        <item name="android:textColor">@color/colorPrimary</item>
        <item name="android:textSize">22sp</item>
    </style>
  • android:colorBackground 属性设置的是溢出菜单的背景颜色;
  • android:textColor 属性设置的是溢出菜单的字体颜色;
  • android:textSize 属性设置的是溢出菜单的字体大小;

而如果想要修改 Toolbar 上的菜单字体颜色和大小则需要使用 android:popupTheme 属性来设置主题,同样继承一个系统主题,然后重写相关属性:

<style name="PopupTheme" parent="ThemeOverlay.AppCompat.Light">
        <item name="android:textSize">22sp</item>
        <item name="actionMenuTextColor">#00FF00</item>
    </style>
  • android:textSize 设置菜单字体大小,如果没有在 android:popupTheme 的主题中修改字体大小,这个属性同样会作用于溢出菜单;
  • actionMenuTextColor 设置菜单的颜色,注意这个是不带命名空间的;

如果在 android:theme 的主题中重写了 android:colorBackgroundandroid:textColor 属性同样可以修改溢出菜单的样式。

自定义View

Toolbar 继承自 ViewGroup,我们可以直接在 Toolbar 中添加其它控件,添加的控件会显示在 Title、SubTitle 和 ActionMenu 之间,如果设置自定义 View 的 android:layout_width="match_parent 会覆盖掉 Title 和 SubTitle。

参考: Toolbar:上位的小三
Material Design 之 Toolbar 开发实践总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,030评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,198评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,995评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,973评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,869评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,766评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,967评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,599评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,886评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,901评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,728评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,504评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,967评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,128评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,445评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,018评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,224评论 2 339

推荐阅读更多精彩内容