整理了一下之前的项目,发现一个关于 Toolbar 的问题,Toolbar 中的 NavigationIconTitle 的距离似乎有点过大:

NavigationIcon 和 Title 间距过大

网上查了一下发现是从 Support 包 V23.0.0 之后才出现这种情况,而我的这个项目使用的是 V28.0.0 的包,看来这个问题历史悠久啊。

打开 Toolbar 的源码:

public class Toolbar extends ViewGroup {
    private RtlSpacingHelper mContentInsets;            // 对应的属性名称为 contentInsetStart
    private int mContentInsetStartWithNavigation;       // 对应的属性名称为 contentInsetStartWithNavigation
    ...
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
        final int paddingLeft = getPaddingLeft();   // 获取系统的偏移量
        int left = paddingLeft;
        ...
        // 计算 Navigation 的 Layout
        if (shouldLayout(mNavButtonView)) {
            if (isRtl) {
                ...
            } else {
                left = layoutChildLeft(mNavButtonView, left, collapsingMargins, alignmentHeight);
            }
        }
        ...
        final int contentInsetLeft = getCurrentContentInsetLeft();
        left = Math.max(left, contentInsetLeft);
        ...
        if (layoutTitle || layoutSubtitle) {
            ...
            if (isRtl) {
                ...
            } else {
                ...
                int titleLeft = left;
                if (layoutTitle) {
                    ...
                    mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom);
                }
                ...
            }
        }
        ...
    }
    ...
    private int layoutChildLeft(View child, int left, int[] collapsingMargins, int alignmentHeight) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final int l = lp.leftMargin - collapsingMargins[0];
        left += Math.max(0, l);
        collapsingMargins[0] = Math.max(0, -l);
        final int top = getChildTop(child, alignmentHeight);
        final int childWidth = child.getMeasuredWidth();
        child.layout(left, top, left + childWidth, top + child.getMeasuredHeight());
        left += childWidth + lp.rightMargin;
        return left;
    }
    
    /**
     * Gets the content inset that will be used on the left side of the bar in the current toolbar configuration.
     *
     * @return the current content inset left in pixels
     * @see #getContentInsetStartWithNavigation()
     * @see #getContentInsetEndWithActions()
     */
    public int getCurrentContentInsetLeft() {
        return isLayoutRtl() ? getCurrentContentInsetEnd() : getCurrentContentInsetStart();
    }

    /**
     * Gets the content inset that will be used on the starting side of the bar in the current toolbar configuration.
     *
     * @return the current content inset start in pixels
     * @see #getContentInsetStartWithNavigation()
     */
    public int getCurrentContentInsetStart() {
        return getNavigationIcon() != null ? Math.max(getContentInsetStart(), Math.max(mContentInsetStartWithNavigation, 0)) : getContentInsetStart();
    }
}

onLayout() 里面,isRtl 这个变量用于判断是否从右往左显示,我们的 Toolbar 是从左往右显示的,所以只需要看值为 false 的情况即可。

在计算 NavigationLayout 中,进到 layoutChildLeft() 方法里进行计算,计算完之后 left 的距离为 paddingLeft + mNavButtonView 的宽度 + mNavButtonView 自身的偏移量。

接下来就进到 getCurrentContentInsetLeft() 方法,因为是从左向右显示所以会调用 getCurrentContentInsetStart() 这个方法,由于有 NavigationIcon,所以就会走前面的分支。

其中 Math.max(mContentInsetStartWithNavigation, 0) 返回的就是 mContentInsetStartWithNavigation 这个值,mContentInsetStartWithNavigation 这个值就是从 contentInsetStartWithNavigation 这个属性中取得的;getContentInsetStart() 这个方法的返回值就是 contentInsetStart 这个属性对应的值。

所以最后就是比较 contentInsetStartcontentInsetStartWithNavigation 这两个属性的值。

接下来我们来看这两个属性的值在修改前后的版本中到底是多少,具体的文件应找到对应版本的 appcompact-v7 包的「arr」文件,然后解压找到 /res/values/values.xml,当然也可以通过『Android Studio』的跳转查找功能。

在 V22.2.0 的版本中,描述 Toolbar 属性的内容如下:

<style name="Base.Widget.AppCompat.Toolbar" parent="android:Widget">
    ...
    <item name="contentInsetStart">16dp</item>
</style>

可以发现 contentInsetStart 的值是 16dp,但没有 contentInsetStartWithNavigation 这个属性,这是因为 contentInsetStartWithNavigation 这个属性是在之后的版本才加上的,而之前的版本 Toolbar 代码中只会根据 contentInsetStart 来计算 Title 的左边距。

再来看看在 V28.0.0 版本中的代码:

<style name="Base.V7.Widget.AppCompat.Toolbar" parent="android:Widget">
    ...
    <item name="contentInsetStart">16dp</item>
    <item name="contentInsetStartWithNavigation">@dimen/abc_action_bar_content_inset_with_nav</item>
</style>

contentInsetStart 的值也还是 16dpcontentInsetStartWithNavigation 这个值定义在 <dimen> 中,我们去找找这个值:

<dimen name="abc_action_bar_content_inset_with_nav">72dp</dimen>

再回到一开始的那段代码:

final int contentInsetLeft = getCurrentContentInsetLeft();      // 核心的方法,返回就是那个让距离错误的值
left = Math.max(left, contentInsetLeft);    // left 会从之前的 left 值也就是计算过 Navigation 的距离之后,和 contentInsetLeft 比较,取最大值

left 的值一开始是 NavigationIcon 的宽度,一般为 56dp,而 contentInsetLeft 这个值是 72dp,取最大值之后 left 的值就变成了 72dp,就最后导致了距离显示异常。

了解了问题产生的原因,就该说说解决方法了,解决方法也很简单,在 Toolbar 控件属性中添加一句即可:

<android.support.v7.widget.Toolbar
    ...
    app:contentInsetStartWithNavigation="0dp" />

也可以通过指定 <style> 的方法:

<style name="NoSpaceActionBarTheme" parent="Base.Widget.AppCompat.Toolbar">
    <item name="contentInsetStart">0dp</item>
    <item name="contentInsetStartWithNavigation">0dp</item>
</style>

再在 Toolbar 控件属性中指定:

<android.support.v7.widget.Toolbar
    ...
    style="@style/NoSpaceActionBarTheme" />

完美解决,看看实际效果图:

NavigationIcon 和 Title 间距正常

不过一时半会真的想不通为什么 Android 要加一个这样的东西…