Android主題適配說明文檔

背景

現(xiàn)項(xiàng)目中涉及紅色掘托、金色主題卦睹,同時(shí)需要適配紅色暗黑畦戒、金色暗黑,本地需要手動(dòng)維護(hù)4套色值结序,并且切換主題時(shí)需要重新銷毀創(chuàng)建頁面障斋,維護(hù)跟用戶體驗(yàn)都不是很友好。

設(shè)計(jì)思路來源

通過調(diào)研,發(fā)現(xiàn)換膚的實(shí)現(xiàn)原理比較符合適用當(dāng)前項(xiàng)目的使用場(chǎng)景,開源項(xiàng)目 Android-Skin-Loader

通過查看源碼換膚實(shí)現(xiàn)原理其實(shí)為 通過下載或者加載本地資源包徐鹤,這里的資源包其實(shí)就是一個(gè)只有資源文件的項(xiàng)目通過編譯打包生成的.apk文件垃环,點(diǎn)擊切換時(shí),通過提前手動(dòng)綁定view和要改變的資源類型 將資源Resource替換成資源包的Resource資源進(jìn)行設(shè)置替換返敬,從而達(dá)到換膚的效果遂庄。

由此整理出方案需要自行實(shí)現(xiàn)的點(diǎn)

  • 獲取需要支持主題切換的view和要改變的屬性類型
  • 資源包不通過.apk的形式存在而是跟正常的資源文件存放的于項(xiàng)目中,可以在XML布局里直接使用
  • 定義需要支持適配的具體屬性 android:textColor|android:background|android:src
  • 自定義屬性配置 是否需要支持切換 以及 是否只區(qū)分暗黑不區(qū)分紅色主題劲赠、金色主題
  • 處理debug開發(fā)的日志以及異常處理情況
  • 支持除textColor 和 background涛目、src,能支持屬性擴(kuò)展
  • 支持動(dòng)態(tài)創(chuàng)建view動(dòng)態(tài)切換主題

具體實(shí)現(xiàn)

前提了解下LayoutInflater原理

LayoutInflatersetFactory(LayoutInflater.Factoryfactory)和setFactory2(LayoutInflater.Factory2 factory)兩個(gè)方法可以讓你去自定義布局的填充(有點(diǎn)類似于過濾器凛澎,我們?cè)谔畛溥@個(gè)View之前可以手動(dòng)綁定view和要改變的資源類型)霹肝,F(xiàn)actory2 是在API 11才添加的。

通過閱讀源碼可以發(fā)現(xiàn),我們?cè)谶M(jìn)入setContentView(R.layout.activity_main)可以看到

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

這里的getDelegate()獲取到的是AppCompatDelegateImpl,我們可以看到

@RestrictTo(LIBRARY)
class AppCompatDelegateImpl extends AppCompatDelegate
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {

實(shí)現(xiàn)了LayoutInflater.Factory2接口,在看 獲取到的是AppCompatDelegateImpl的

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

LayoutInflater.from(mContext)獲取最終獲取的是ontext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)

 /**
    * Obtains the LayoutInflater from the given context.
    */
   public static LayoutInflater from(Context context) {
       LayoutInflater LayoutInflater =
               (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
       if (LayoutInflater == null) {
           throw new AssertionError("LayoutInflater not found.");
       }
       return LayoutInflater;
   }

繼續(xù)往里看會(huì)找到

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                advanceToRootNode(parser);
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

可以看到如果不是merge標(biāo)簽會(huì)通過createViewFromTag(root,name,inflaterContext, attrs)創(chuàng)建view塑煎,找到方法

 @UnsupportedAppUsage
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        try {
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

最終我們?cè)趖ryCreateView(@Nullable View parent, @NonNull String name,@NonNull Context context,@NonNull AttributeSet attrs)里看到

 @UnsupportedAppUsage(trackingBug = 122360734)
    @Nullable
    public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        return view;
    }

最終是調(diào)用的LayoutInflater的setFactory2()方法創(chuàng)建View沫换,當(dāng)我們不手動(dòng)調(diào)用設(shè)置Factory2,我們還記得前面說的AppCompatDelegateImpl實(shí)現(xiàn)了LayoutInflater.Factory2接口重寫了createView()方法并設(shè)置了

@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }
@Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        if (mAppCompatViewInflater == null) {
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
            String viewInflaterClassName =
                    a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
            if ((viewInflaterClassName == null)
                    || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
                // Either default class name or set explicitly to null. In both cases
                // create the base inflater (no reflection)
                mAppCompatViewInflater = new AppCompatViewInflater();
            } else {
                try {
                    Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
                    mAppCompatViewInflater =
                            (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                    .newInstance();
                } catch (Throwable t) {
                    Log.i(TAG, "Failed to instantiate custom view inflater "
                            + viewInflaterClassName + ". Falling back to default.", t);
                    mAppCompatViewInflater = new AppCompatViewInflater();
                }
            }
        }

發(fā)現(xiàn)具體是通過AppCompatViewInflater的createView()去創(chuàng)建的View具體

final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;

        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
        // by using the parent's context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }
        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Spinner":
                view = createSpinner(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageButton":
                view = createImageButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckBox":
                view = createCheckBox(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RadioButton":
                view = createRadioButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckedTextView":
                view = createCheckedTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "AutoCompleteTextView":
                view = createAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "MultiAutoCompleteTextView":
                view = createMultiAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RatingBar":
                view = createRatingBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "SeekBar":
                view = createSeekBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ToggleButton":
                view = createToggleButton(context, attrs);
                verifyNotNull(view, name);
                break;
            default:
                // The fallback that allows extending class to take over view inflation
                // for other tags. Note that we don't check that the result is not-null.
                // That allows the custom inflater path to fall back on the default one
                // later in this method.
                view = createView(context, name, attrs);
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

通過上面的了解我們可以大概知道可以通過自定義一個(gè)ThemeInflaterFactory implements LayoutInflater.Factory2用來解析XML布局創(chuàng)建View(相當(dāng)于hook主系統(tǒng)創(chuàng)建view的過程)最铁。這里我們可以合理的保存下來需要適配主題的view以及屬性

ThemeInflaterFactory的功能 具體邏輯可以查看代碼

  • 維護(hù)一個(gè)mThemeItemMap集合讯赏,用來保存需要適配的view 以及屬性垮兑,并對(duì)外提供添加跟清除方法
  • ThemeItem對(duì)象包含一個(gè)View,以及需要修改的屬性擴(kuò)展BaseAttr漱挎,具體實(shí)現(xiàn)當(dāng)前有BackgroundAttr甥角、ImageViewSrcAttr、TextColorAttr
  • 對(duì)外提供applyTheme方法识樱,遍歷集合mThemeItemMap,修改屬性值
 @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        boolean isThemeEnable = attrs.getAttributeBooleanValue(ThemeConfig.NAMESPACE, ThemeConfig.ATTR_THEME_ENABLE, false);
        //調(diào)用系統(tǒng)創(chuàng)建基本控件
        AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
        View view = delegate.createView(parent, name, context, attrs);
        if (isThemeEnable || ThemeConfig.isGlobalSkinApply()) { //控件支持切換模式 或者開啟類全局支持開關(guān)
            if (view == null) {
                view = ViewCreate.createViewFromTag(context, name, attrs);
            }
            if (view == null) {
                return null;
            }
            parseSkinAttr(context, attrs, view);
        }
        return view;
    }
 private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
        List<BaseAttr> viewAttrs = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attrName = attrs.getAttributeName(i);
            String attrValue = attrs.getAttributeValue(i);
            if ("style".equals(attrName)) { //mxl布局引入style
                int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background};
                TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);
                int textColorId = a.getResourceId(0, -1);
                int backgroundId = a.getResourceId(1, -1);
                if (textColorId != -1) {
                    String entryName = context.getResources().getResourceEntryName(textColorId);
                    String typeName = context.getResources().getResourceTypeName(textColorId);
                    BaseAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);
                    if (skinAttr != null) {
                        viewAttrs.add(skinAttr);
                    }
                }
                if (backgroundId != -1) {
                    String entryName = context.getResources().getResourceEntryName(backgroundId);
                    String typeName = context.getResources().getResourceTypeName(backgroundId);
                    BaseAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);
                    if (skinAttr != null) {
                        viewAttrs.add(skinAttr);
                    }
                }
                a.recycle();
                continue;
            }
            if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
                try {
                    int id = Integer.parseInt(attrValue.substring(1));
                    if (id == 0) {
                        continue;
                    }
                    boolean isOnlyDark = attrs.getAttributeBooleanValue(ThemeConfig.NAMESPACE, ThemeConfig.ATTR_THEME_ONLY_DARK, false);
                    String entryName = context.getResources().getResourceEntryName(id);
                    String typeName = context.getResources().getResourceTypeName(id);
                    BaseAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName, isOnlyDark);
                    if (mSkinAttr != null) {
                        viewAttrs.add(mSkinAttr);
                    }
                } catch (NumberFormatException e) {
                }
            }
        }
        if (!viewAttrs.isEmpty()) {
            ThemeItem skinItem = new ThemeItem();
            skinItem.view = view;
            skinItem.attrs = viewAttrs;
            mThemeItemMap.put(skinItem.view, skinItem);
            if (!AppThemeManager.getInstance().isNormalTheme() || AppThemeManager.getInstance().isInNightTheme()) {
                skinItem.changeTheme();
            }
        }
    }

view 支持主題切換或者全局支持開關(guān)打開,保存對(duì)應(yīng)的view以及屬性AttrFactory獲取的BaseAttr的實(shí)現(xiàn)類到ThemeItem里,保存于mThemeItemMap集合里

 /**
     * 運(yùn)用主題
     */
    public void applyTheme() {
        if (mThemeItemMap.isEmpty()) {
            return;
        }
        for (View view : mThemeItemMap.keySet()) {
            if (view == null) {
                continue;
            }
            mThemeItemMap.get(view).changeTheme();
        }
    }

對(duì)外提供方法修改主題

AppThemeManager的功能

  • 維護(hù)一個(gè)主題切換監(jiān)聽集合List<IThemeUpdate>mThemeObservers,在baseActivity實(shí)現(xiàn)IThemeUpdate接口,并調(diào)用AppThemeManager的addObserver(IThemeUpdate observer)添加監(jiān)聽震束,對(duì)應(yīng)onDestory移除監(jiān)聽,
  • 點(diǎn)擊切換主題時(shí)調(diào)用notifyThemeUpdate()方法遍歷集合通知各個(gè)頁面調(diào)用ThemeInflaterFactory對(duì)外提供applyTheme方法修改對(duì)應(yīng)的屬性資源值

ThemeResourceUtil的功能

  • 通過編譯后的資源在R文件的id怜庸,獲取到資源名稱,在根據(jù)當(dāng)前主題拼接資源名稱垢村,再獲取R文件對(duì)應(yīng)拼接后到資源id返回供BaseAttr的 applyTheme(view)設(shè)置切換后的資源達(dá)到切換主題的功能
  • ThemeConfig 配置信息割疾,包括是否開啟全局view支持主題切換開關(guān)\debug模式開關(guān)\狀態(tài)欄適配開關(guān)\本地主題記錄sp等

具體使用

1.在Application初始化AppThemeManager以及配置ThemeConfig

public class DarkApplication extends Application {
  @Override
  public void onCreate() {
      super.onCreate();
      AppThemeManager.getInstance().init(this);
      ThemeConfig.setCanChangeStatusColor(true);
      ThemeConfig.enableGlobalThemeApply();
      ThemeConfig.setDebug(true);
  }
}

2.BaseActivity實(shí)現(xiàn)IThemeUpdate, IDynamicNewView 接口

  public class BaseActivity extends AppCompatActivity implements IThemeUpdate, IDynamicNewView {

    /**
     * 自定義 InflaterFactory
     */
    private ThemeInflaterFactory mThemeInflaterFactory;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mThemeInflaterFactory = new ThemeInflaterFactory(this);
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), mThemeInflaterFactory);
        super.onCreate(savedInstanceState);
        AppThemeManager.getInstance().addObserver(this);
        changeStatusColor();

    }

    public void changeStatusColor() {
        if (!ThemeConfig.isCanChangeStatusColor()) {
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            int color = ThemeResourceUtil.getColorPrimaryDark();
            if (color != -1) {
                Window window = getWindow();
                window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
                window.setStatusBarColor(ThemeResourceUtil.getColorPrimaryDark());
            }
        }
    }

    public ThemeInflaterFactory getInflaterFactory() {
        return mThemeInflaterFactory;
    }

    @Override
    public void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
        mThemeInflaterFactory.dynamicAddThemeEnableView(this, view, pDAttrs);
    }

    @Override
    public void dynamicAddView(View view, String attrName, int attrValueResId, boolean isOnlyDark) {
        mThemeInflaterFactory.dynamicAddThemeEnableView(this, view, attrName, attrValueResId, isOnlyDark);
    }

    @Override
    public void onThemeUpdate() {
        mThemeInflaterFactory.applyTheme();
        changeStatusColor();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        AppThemeManager.getInstance().removeObserver(this);
        mThemeInflaterFactory.clean();
    }
}

注意 setFactory2()一定要在 super.onCreate(savedInstanceState)之前調(diào)用即setContentView之前設(shè)置Factory2
3.在需要切換主題的根布局上添加 <code>xmlns:theme="http://schemas.android.com/android/theme" </code>,然后在需要切換主題的View上加上 <code>theme:enable="true" </code>嘉栓,注意<code>theme:onlyDark="true" </code>的使用場(chǎng)景是紅色主題 和金色主題都使用的同一資源宏榕,不需要區(qū)分,只區(qū)分暗黑資源侵佃。不寫默認(rèn)為false

4.資源文件下創(chuàng)建對(duì)應(yīng)資源區(qū)分colors.xml麻昼、colors-night.xml、colors_gold.xml馋辈、colors_gold_night.xml 或者mipmap_img.png抚芦、mipmap_img_night.png、mipmap_img_gold.png迈螟、mipmap_img_gold_night.png
具體獲取的方法看參考ThemeResourceUtil

主題屬性擴(kuò)展

默認(rèn)支持 textColor 和 background叉抡、src的主題切換。如果你還需要對(duì)其他屬性進(jìn)行主題切換答毫,需要去自定義了
比如 TabLayout它下面會(huì)有一個(gè)指示器褥民,當(dāng)我們換主題的時(shí)候也希望這個(gè)指示器的顏色也跟著更改。

  • 第一步
public class TabLayoutIndicatorAttr extends BaseAttr {
    @Override
    public void applyTheme(View view) {
        if (view instanceof TabLayout) {
            TabLayout tl = (TabLayout) view;
            if (RES_TYPE_NAME_COLOR.equals(attrValueTypeName)) {
                int color = ThemeResourceUtil.getColor(attrValueRefId);
                tl.setSelectedTabIndicatorColor(color);
            }
        }
    }
}
  • 第二步
    方法中加入<code> ThemeConfig.addSupportAttr("tabLayoutIndicator", new TabLayoutIndicatorAttr());</code>
  • 最后我們就可以正常使用了洗搂,<code>dynamicAddView(tablayout, "tabLayoutIndicator", R.color.colorPrimaryDark);</code>

dynamicAddView:當(dāng)動(dòng)態(tài)創(chuàng)建的View也需要主題切換的時(shí)候,就可以調(diào)用dynamicAddView

注意事項(xiàng)

  1. 主題切換默認(rèn)只支持android的常用控件消返,支持庫的控件和自定義控件需要?jiǎng)討B(tài)添加(如: <code>dynamicAddView(toolbar, "background", R.color.colorPrimaryDark);</code>),在布局文件中使用<code>theme:enable="true"</code>是無效的
  2. 默認(rèn)不支持狀態(tài)欄顏色的更改蚕脏,如果需要主題切換的同時(shí)也要更改狀態(tài)欄顏色侦副,在Application中配置<code>ThemeConfig.setCanChangeStatusColor(true);</code>,狀態(tài)欄的顏色值來源于<code>colorPrimaryDark</code>
    3.有主題切換需求 View 所使用的資源一定要是引用值驼鞭,如:@color/red秦驯,而不是 #ff0000

demo效果截圖

WechatIMG17.png
WechatIMG18.png
WechatIMG19.png
WechatIMG20.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市挣棕,隨后出現(xiàn)的幾起案子译隘,更是在濱河造成了極大的恐慌亲桥,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件固耘,死亡現(xiàn)場(chǎng)離奇詭異题篷,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)厅目,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門番枚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人损敷,你說我怎么就攤上這事葫笼。” “怎么了拗馒?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵路星,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我诱桂,道長(zhǎng)洋丐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任挥等,我火速辦了婚禮友绝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肝劲。我一直安慰自己九榔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布涡相。 她就那樣靜靜地躺著哲泊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪催蝗。 梳的紋絲不亂的頭發(fā)上切威,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音丙号,去河邊找鬼先朦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛犬缨,可吹牛的內(nèi)容都是我干的喳魏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼怀薛,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼刺彩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤创倔,失蹤者是張志新(化名)和其女友劉穎嗡害,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體畦攘,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡霸妹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了知押。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叹螟。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖台盯,靈堂內(nèi)的尸體忽然破棺而出首妖,到底是詐尸還是另有隱情,我是刑警寧澤爷恳,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站象踊,受9級(jí)特大地震影響温亲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杯矩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一栈虚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧史隆,春花似錦魂务、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至熔酷,卻和暖如春孤紧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拒秘。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工号显, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人躺酒。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓押蚤,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親羹应。 傳聞我的和親對(duì)象是個(gè)殘疾皇子揽碘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容