Toolbar中style的自定义及加载过程

Github Demo:https://github.com/imflyn/ActionBarStyle


不久前,公司里设计师大大踌躇满志的说我们的app要改版。首当其冲的就是主题色的改变,由红色改为白色。并且界面改为扁平风格。那么意味着需要对所有界面里的ActionBar或者ToolBar都要进行主题样式与elevation的修改。最后的结果如下图:

Toolbar中style的自定义及加载过程_第1张图片

最简便的方法自然是定义style来更换,如果都在Java代码里去改的话工作量变多不说且都是重复的劳动。也不利于代码的维护。

一.定义Style

1.ActionBar的Style定义

4.4及以下版本style.xml:





5.0及以上版本的style.xml:




主题色:


文字样式的定义:




在Activity中引用style:


注意:Android4.4版本及以下ActionBar取消elevation必须设置windowContentOverlay属性为null,这样背景就是默认的灰色了,所以还需要自己定义ActionBar的背景,如果在ActionBar底部需要分割线还要做一个.9的图片设置为ActionBar的背景。

2.ToolBar的Style定义

在style.xml中定义ToolBar样式:





在layout文件中设置ToolBar的文字样式:



    

        
    

在Activity中引用style,并且Activity继承supportV7包中的AppCompatActivity


注意:Android4.4版本及以下因为不支持elevation属性,所以需要添加下面的代码才能显示ToolBar底部的横线。R.drawable.bg_frame是最底部为横线,背景透明的.9图片:

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
    ViewCompat.setBackground(findViewById(R.id.appbar), ContextCompat.getDrawable(this, R.drawable.bg_frame));
}

在Demo中最后的效果如下图,可以看到返回按钮、Title、Menu上的文字大小和颜色都是style定义后的。


Toolbar中style的自定义及加载过程_第2张图片

二.遇到的坑

  1. 定义ActionBar样式时,4.4及以下版本手机设置了@null后,必须为ActionBar设置背景,否则ActionBar是默认的颜色
  • 4.4及以下版本手机不支持elevation属性,所以在ActionBar底部的分割线或阴影效果需要自己做图片。
  • Menu菜单的文字颜色必须通过actionMenuTextColor属性设置,在actionMenuTextAppearance属性中设置textColor无效。
  • ToolBar的文字样式titleTextAppearance必须在layout布局文件中引用,如果像Actionbar一样只在style.xml中定义是无效的。
  • AppBarLayout的背景和elevation是由stateListAnimator控制的,如果需要改变elevation高度必须自定义stateListAnimator。因为stateListAnimator是5.0版本后的属性,4.4及以下版本手机必须重新设置AppBarLayout的background属性。

三.源码之下,了无秘密

修改Actionbar的样式固然很快,但是为了知道为什么会有上面写到的在定义style属性时遇到的坑,所以带着问题看看源码,在源码之下我们可以了解到ActionBar或ToolBar中的Style在AppCompatActivity中是如何被加载的。
注意:下面贴出的源码不是完整的google官方代码,只截取了关键部分。

首先AppCompatActivity在执行onCreate方法时创建了AppCompatDelegate对象,并进行了AppCompatDelegate的初始化。
AppCompatDelegate相当于一个委托,appcompat适配包中一些方法委托AppCompatDelegate来调用。

protected void onCreate(@Nullable Bundle savedInstanceState) {  
    final AppCompatDelegate delegate = getDelegate();//执行AppCompatDelegate.create();
    //初始化工作
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    super.onCreate(savedInstanceState);
}

//根据Android系统版本创建AppCompatDelegate对象
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) {
    final int sdk = Build.VERSION.SDK_INT;
    if (BuildCompat.isAtLeastN()) {
        return new AppCompatDelegateImplN(context, window, callback);
    } else if (sdk >= 23) {
        return new AppCompatDelegateImplV23(context, window, callback);
    } else if (sdk >= 14) {
        return new AppCompatDelegateImplV14(context, window, callback);
    } else if (sdk >= 11) {
        return new AppCompatDelegateImplV11(context, window, callback);
    } else {
        return new AppCompatDelegateImplV9(context, window, callback);
    }
}

onCreate结束之后,在Activity中调用setContentView()方法后,AppCompatActivity中会执行ensureSubDecor()方法,这个方法具体做了ActionBar窗体的创建,并使ActionBar依附到屏幕的Window中去。

public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        onSubDecorInstalled(mSubDecor);
        mSubDecorInstalled = true;
    }
}

private ViewGroup createSubDecor() {
    //加载Activity的主题
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
    if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
        a.recycle();
        throw new IllegalStateException(
                "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
    }
    //读取windowNoTitle属性,判断是否需要ActionBar
    if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
        // Don't allow an action bar if there is no title.
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
    }
    //读取windowActionBarOverlay属性
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
    }
    //读取windowActionModeOverlay属性
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
        requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
    }
    //读取android:windowIsFloating属性
    mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
    a.recycle();

    // Now let's make sure that the Window has installed its decor by retrieving it
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;
    
    if (!mWindowNoTitle) {
        //当需要ActionBar时执行下面的逻辑
        if (mIsFloating) {
            //省略
        } else if (mHasActionBar) {
            //读取ActionBar主题
            TypedValue outValue = new TypedValue();
            mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
            //加载ActionBar窗体的布局文件R.layout.abc_screen_toolbar
            subDecor = (ViewGroup) LayoutInflater.from(themedContext).inflate(R.layout.abc_screen_toolbar, null);
            mDecorContentParent = (DecorContentParent) subDecor.findViewById(R.id.decor_content_parent);
            mDecorContentParent.setWindowCallback(getWindowCallback());
        }
    } else {
        //省略
    }

    //在该方法的最后会把ActionBar的窗体加载到屏幕的整个Window中去
    mWindow.setContentView(subDecor);
    return subDecor;
}

上面的代码我们看到映射了一个名为abc_screen_toolbar.xml的布局文件,在这个xml布局文件中引用了Toolbar,并且可以看到声明了属性style="?attr/toolbarStyle",那么是不是这个toolBarStyle就决定了ToolBar的样式呢?我们继续往下看。



    

    

        

        

    

接下来会执行Toolbar的构造函数,其中引用的style也是在xml布局文件里所声明的。

public Toolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    final TintTypedArray a = TintTypedArray.obtainStyledAttributes(getContext(), attrs,R.styleable.Toolbar, defStyleAttr, 0);    
    //标题文字样式
    mTitleTextAppearance = a.getResourceId(R.styleable.Toolbar_titleTextAppearance, 0);
    mSubtitleTextAppearance = a.getResourceId(R.styleable.Toolbar_subtitleTextAppearance, 0);   
    //标题
    final CharSequence title = a.getText(R.styleable.Toolbar_title);
    if (!TextUtils.isEmpty(title)) {
        setTitle(title);
    } 
    //设置返回按钮的Icon
    final Drawable navIcon = a.getDrawable(R.styleable.Toolbar_navigationIcon);
    if (navIcon != null) {
        setNavigationIcon(navIcon);
    }
    //设置title文字颜色
    if (a.hasValue(R.styleable.Toolbar_titleTextColor)) {
        setTitleTextColor(a.getColor(R.styleable.Toolbar_titleTextColor, 0xffffffff));
    }
    //设置subtitle文字颜色
    if (a.hasValue(R.styleable.Toolbar_subtitleTextColor)) {
        setSubtitleTextColor(a.getColor(R.styleable.Toolbar_subtitleTextColor, 0xffffffff));
    }
}

Toolbar构造完成后绘制ActionBarOverlayLayout时,会调用ActionBarOverlayLayout中的pullChildren()与getDecorToolbar()两个方法,在为全局变量mDecorToolbar 赋值时会创建一个Toolbar的包装器ToolbarWidgetWrapper

void pullChildren() {
    if (mContent == null) {
        mContent = (ContentFrameLayout) findViewById(R.id.action_bar_activity_content);
        mActionBarTop = (ActionBarContainer) findViewById(R.id.action_bar_container);
        mDecorToolbar = getDecorToolbar(findViewById(R.id.action_bar));
    }
}

private DecorToolbar getDecorToolbar(View view) {
       if (view instanceof Toolbar) {
        return ((Toolbar) view).getWrapper();
       } 
}

public DecorToolbar getWrapper() {
    if (mWrapper == null) {
        mWrapper = new ToolbarWidgetWrapper(this, true);
    }
    return mWrapper;
}

在Toolbar的包装器ToolbarWidgetWrapper中,会加载actionBarStyle这个style中的属性,如homeAsUpIndicator定义了回退键的图片,titleTextStyle定义了Title的样式。所以ActionBar的样式最后加载的style都是在这个ToolbarWidgetWrapper中完成的。

public ToolbarWidgetWrapper(Toolbar toolbar, boolean style,int defaultNavigationContentDescription, int defaultNavigationIcon) {
    mToolbar = toolbar;
    mTitle = toolbar.getTitle();
    mSubtitle = toolbar.getSubtitle();
    mTitleSet = mTitle != null;
    mNavIcon = toolbar.getNavigationIcon();
    final TintTypedArray a = TintTypedArray.obtainStyledAttributes(toolbar.getContext(), null, R.styleable.ActionBar, R.attr.actionBarStyle, 0);
    mDefaultNavigationIcon = a.getDrawable(R.styleable.ActionBar_homeAsUpIndicator);
    if (style) {
        final CharSequence title = a.getText(R.styleable.ActionBar_title);
        if (!TextUtils.isEmpty(title)) {
            setTitle(title);
        }

        final CharSequence subtitle = a.getText(R.styleable.ActionBar_subtitle);
        if (!TextUtils.isEmpty(subtitle)) {
            setSubtitle(subtitle);
        }

        final Drawable logo = a.getDrawable(R.styleable.ActionBar_logo);
        if (logo != null) {
            setLogo(logo);
        }

        final Drawable icon = a.getDrawable(R.styleable.ActionBar_icon);
        if (icon != null) {
            setIcon(icon);
        }
        if (mNavIcon == null && mDefaultNavigationIcon != null) {
            setNavigationIcon(mDefaultNavigationIcon);
        }
        setDisplayOptions(a.getInt(R.styleable.ActionBar_displayOptions, 0));

        final int customNavId = a.getResourceId(
                R.styleable.ActionBar_customNavigationLayout, 0);
        if (customNavId != 0) {
            setCustomView(LayoutInflater.from(mToolbar.getContext()).inflate(customNavId,mToolbar, false));
            setDisplayOptions(mDisplayOpts | ActionBar.DISPLAY_SHOW_CUSTOM);
        }

        final int height = a.getLayoutDimension(R.styleable.ActionBar_height, 0);
        if (height > 0) {
            final ViewGroup.LayoutParams lp = mToolbar.getLayoutParams();
            lp.height = height;
            mToolbar.setLayoutParams(lp);
        }

        final int contentInsetStart = a.getDimensionPixelOffset(R.styleable.ActionBar_contentInsetStart,  -1);
        final int contentInsetEnd = a.getDimensionPixelOffset(R.styleable.ActionBar_contentInsetEnd,  -1);
        if (contentInsetStart >= 0 || contentInsetEnd >= 0) {
            mToolbar.setContentInsetsRelative(Math.max(contentInsetStart, 0), Math.max(contentInsetEnd, 0));
        }

        final int titleTextStyle = a.getResourceId(R.styleable.ActionBar_titleTextStyle, 0);
        if (titleTextStyle != 0) {
            mToolbar.setTitleTextAppearance(mToolbar.getContext(), titleTextStyle);
        }

        final int subtitleTextStyle = a.getResourceId(R.styleable.ActionBar_subtitleTextStyle, 0);
        if (subtitleTextStyle != 0) {
            mToolbar.setSubtitleTextAppearance(mToolbar.getContext(), subtitleTextStyle);
        }

        final int popupTheme = a.getResourceId(R.styleable.ActionBar_popupTheme, 0);
        if (popupTheme != 0) {
            mToolbar.setPopupTheme(popupTheme);
        }
    } else {
        mDisplayOpts = detectDisplayOptions();
    }
    a.recycle();
}

最后在调用getSupportActionBar()时会进入initWindowDecorActionBar()方法。ActionBar是一个抽象类,WindowDecorActionBar则是ActionBar的具体实现。在init过程中看到了对elevation的设置。

public void initWindowDecorActionBar() {
    ensureSubDecor();
    if (!mHasActionBar || mActionBar != null) {
        return;
    }
    if (mOriginalWindowCallback instanceof Activity) {
        mActionBar = new WindowDecorActionBar((Activity) mOriginalWindowCallback,
                mOverlayActionBar);
    }   
}

public WindowDecorActionBar(Activity activity, boolean overlayMode) {
    mActivity = activity;
    Window window = activity.getWindow();
    View decor = window.getDecorView();
    init(decor);
    if (!overlayMode) {
        mContentView = decor.findViewById(android.R.id.content);
    }
}

private void init(View decor) {
    mOverlayLayout = (ActionBarOverlayLayout) decor.findViewById(R.id.decor_content_parent);

    mDecorToolbar = getDecorToolbar(decor.findViewById(R.id.action_bar));
    mContextView = (ActionBarContextView) decor.findViewById(R.id.action_context_bar);
    mContainerView = (ActionBarContainer) decor.findViewById(R.id.action_bar_container);

    mContext = mDecorToolbar.getContext();
    
    final int elevation = a.getDimensionPixelSize(R.styleable.ActionBar_elevation, 0);
    if (elevation != 0) {
        setElevation(elevation);
    }
    a.recycle();
}

public void setElevation(float elevation) {
    ViewCompat.setElevation(mContainerView, elevation);
}

如果ActionBar上有Menu时,会调用ToolbarWidgetWrapper中setMenu方法,执行ActionMenuPresenter的构造函数,ActionMenuPresenter构造函数中第二个参数是每个menu item的父布局,第三个参数就对应某个menu item的布局。Menu item的xml布局中引用了actionMenuTextAppearance actionMenuTextColor两个属性,所以知道了,menu item的字体和颜色是由这两个属性控制的。

public void setMenu(Menu menu, MenuPresenter.Callback cb) {
    if (mActionMenuPresenter == null) {
        mActionMenuPresenter = new ActionMenuPresenter(mToolbar.getContext());
        mActionMenuPresenter.setId(R.id.action_menu_presenter);
    }
    mActionMenuPresenter.setCallback(cb);
    mToolbar.setMenu((MenuBuilder) menu, mActionMenuPresenter);
}

public ActionMenuPresenter(Context context) {
    super(context, R.layout.abc_action_menu_layout, R.layout.abc_action_menu_item_layout);
}


为什么在style.xml中定义toolbar的style无效,只能在toolbar的布局文件中引用呢?是因为Toolbar读取的是layout布局文件中的style,并且在构造ToolbarWidgetWrapper对象时也并不会和ActionBar一样去读取actionbar的属性。因为在调用setSupportActionBar()后会构造一个ToolbarActionBar,ToolbarActionBar中又会构造一个ToolbarWidgetWrapper,而ToolbarWidgetWrapper的构造函数中第二个参数在源码中传入的是false,所以不会在ToolbarWidgetWrapper进行style的加载,只会在创建Toolbar时进行style的加载。

public void setSupportActionBar(Toolbar toolbar) {
    if (toolbar != null) {
        final ToolbarActionBar tbab = new ToolbarActionBar(toolbar,((Activity) mOriginalWindowCallback).getTitle(), mAppCompatWindowCallback);
    }
    invalidateOptionsMenu();
}

public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback callback) {
    mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false);
}

public ToolbarWidgetWrapper(Toolbar toolbar, boolean style,int defaultNavigationContentDescription, int defaultNavigationIcon) {   
    if (style) {
        //不执行
        final CharSequence title = a.getText(R.styleable.ActionBar_title);
        if (!TextUtils.isEmpty(title)) {
            setTitle(title);
        }

        final CharSequence subtitle = a.getText(R.styleable.ActionBar_subtitle);
        if (!TextUtils.isEmpty(subtitle)) {
            setSubtitle(subtitle);
        }
       ...
    } else {
        mDisplayOpts = detectDisplayOptions();
    }
    a.recycle();
}

最后:

更详细的参考Demo在github中,如果有错误也希望大家能够指出,觉得能帮到你的话给个Star吧。

你可能感兴趣的:(Toolbar中style的自定义及加载过程)