Android 简单学习开源换肤框架(ThemeSkinning)

Android 简单学习开源换肤框架(ThemeSkinning)

GitHub地址 ThemeSkinning

找到初始化View的入口并替换自定义的入口

通常我们都是通过 setContentView(int ID)把View 加载到我们的Activity当中,因此我们可以一步一步的打开源码去查看framework 是如何帮助我们初始化这些View 。一步一步的往下看可以看出。

//先埋下伏笔 AppCompatActivity  this.getDelegate()
public void setContentView(@LayoutRes int layoutResID) {
        this.getDelegate().setContentView(layoutResID);
    }
//我们继续往下点进去
public void setContentView(int resId) {
        this.ensureSubDecor();
        ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
        contentParent.removeAllViews();
        LayoutInflater.from(this.mContext).inflate(resId, contentParent);  //通过这里来解析我们的xml ,从这里继续往下看
        this.mOriginalWindowCallback.onContentChanged();
    }

//inflate 最终走到了这里。 重要的点就是 final View temp = createViewFromTag(root, name, inflaterContext, attrs);
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 {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                //xml包含 merge 标签 
                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");
                    }
                    
                    //这个方法最终也会调用 createViewFromTag 方法
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    // 使用提供的属性集从标记名创建视图。 通过标签来解析View,我们着重看这个方法具体干了什么
                    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(parser.getPositionDescription()
                        + ": " + 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;
        }
    }
//这个方法中  onCreateView出现多次,从字面意思应该可以看出 onCreateView用来创建View 
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();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

    
        try {
            View view;
            //如果这里 mFactory2 不为空就走这的方法  这里我们可以看看什么时候进行初始化  
            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);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    
                    // 判断是不是自定义View 
                    // 1.自定义View在布局文件中是全类名
  				  // 2.而系统的View则不是全类名
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs); //通过反射创建View 因此布局复杂的情况下会很影响性能
                    } else {
                        view = createView(name, null, attrs);   //通过反射创建View 因此布局复杂的情况下会很影响性能
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

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

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

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }
//(换肤这个框架用的是AppCompatActivity)mFactory2是个接口 ,看看这个接口什么时候进行初始化。
//还记得刚刚我们第一次点进去的this.getDelegate().setContentView 我们点进去看看这个this.getDelegate() 做了什么。
@NonNull
    public AppCompatDelegate getDelegate() {
        if (this.mDelegate == null) {
            this.mDelegate = AppCompatDelegate.create(this, this);
        }
        return this.mDelegate;
    }

//AppCompatDelegate是个抽象类,通过AppCompatDelegate#create方法可以知道该抽象类的实现类为AppCompatDelegateImpl
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) {
        if (VERSION.SDK_INT >= 24) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (VERSION.SDK_INT >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (VERSION.SDK_INT >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else {
            return (AppCompatDelegate)(VERSION.SDK_INT >= 11 ? new AppCompatDelegateImplV11(context, window, callback) : new AppCompatDelegateImplV9(context, window, callback));
        }
    }

//getDelegate()在哪里运用了 最终我们可以看到 AppCompatActivity  onCreate 
protected void onCreate(@Nullable Bundle savedInstanceState) {
        AppCompatDelegate delegate = this.getDelegate();
    
        delegate.installViewFactory();  //抽象类安装Factory()
    
        delegate.onCreate(savedInstanceState);
        if (delegate.applyDayNight() && this.mThemeId != 0) {
            if (VERSION.SDK_INT >= 23) {
                this.onApplyThemeResource(this.getTheme(), this.mThemeId, false);
            } else {
                this.setTheme(this.mThemeId);
            }
        }
        super.onCreate(savedInstanceState);
    }

//AppCompatDelegateImp 实现了 Factory2接口(实现类) 找到这个方法 
public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
        if (layoutInflater.getFactory() == null) {
            //这里深挖进去是通过反射把 Factory2赋值给 layoutInflater有兴趣可以继续看
            LayoutInflaterCompat.setFactory2(layoutInflater, this);  
            
        } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
            Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
        }
    }
//前面我们提到
//如果这里 mFactory2 不为空就走这的方法 
if (mFactory2 != null) {
     view = mFactory2.onCreateView(parent, name, context, attrs);
   }

//AppCompatDelegateImp 实现了Factory2接口  找到这个方法 onCreateView
public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        if (this.mAppCompatViewInflater == null) {
            this.mAppCompatViewInflater = new AppCompatViewInflater();
        }

        boolean inheritContext = false;
        if (IS_PRE_LOLLIPOP) {
            inheritContext = attrs instanceof XmlPullParser ? ((XmlPullParser)attrs).getDepth() > 1 : this.shouldInheritContext((ViewParent)parent);
        }

        //着重看这个做了什么 this.mAppCompatViewInflater.createView
        return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
    }

//AppCompatViewInflater  createView
//这里判断name 新建View 
public final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }

        if (readAndroidTheme || readAppTheme) {
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }

        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = null;
        byte var12 = -1;
        switch(name.hashCode()) {
        case -1946472170:
            if (name.equals("RatingBar")) {
                var12 = 11;
            }
            break;
        case -1455429095:
            if (name.equals("CheckedTextView")) {
                var12 = 8;
            }
            break;
        case -1346021293:
            if (name.equals("MultiAutoCompleteTextView")) {
                var12 = 10;
            }
            break;
        case -938935918:
            if (name.equals("TextView")) {
                var12 = 0;
            }
            break;
        case -937446323:
            if (name.equals("ImageButton")) {
                var12 = 5;
            }
            break;
        case -658531749:
            if (name.equals("SeekBar")) {
                var12 = 12;
            }
            break;
        case -339785223:
            if (name.equals("Spinner")) {
                var12 = 4;
            }
            break;
        case 776382189:
            if (name.equals("RadioButton")) {
                var12 = 7;
            }
            break;
        case 1125864064:
            if (name.equals("ImageView")) {
                var12 = 1;
            }
            break;
        case 1413872058:
            if (name.equals("AutoCompleteTextView")) {
                var12 = 9;
            }
            break;
        case 1601505219:
            if (name.equals("CheckBox")) {
                var12 = 6;
            }
            break;
        case 1666676343:
            if (name.equals("EditText")) {
                var12 = 3;
            }
            break;
        case 2001146706:
            if (name.equals("Button")) {
                var12 = 2;
            }
        }

        switch(var12) {
        case 0:
            view = new AppCompatTextView(context, attrs);
            break;
        case 1:
            view = new AppCompatImageView(context, attrs);
            break;
        case 2:
            view = new AppCompatButton(context, attrs);
            break;
        case 3:
            view = new AppCompatEditText(context, attrs);
            break;
        case 4:
            view = new AppCompatSpinner(context, attrs);
            break;
        case 5:
            view = new AppCompatImageButton(context, attrs);
            break;
        case 6:
            view = new AppCompatCheckBox(context, attrs);
            break;
        case 7:
            view = new AppCompatRadioButton(context, attrs);
            break;
        case 8:
            view = new AppCompatCheckedTextView(context, attrs);
            break;
        case 9:
            view = new AppCompatAutoCompleteTextView(context, attrs);
            break;
        case 10:
            view = new AppCompatMultiAutoCompleteTextView(context, attrs);
            break;
        case 11:
            view = new AppCompatRatingBar(context, attrs);
            break;
        case 12:
            view = new AppCompatSeekBar(context, attrs);
        }

    
        //自定义View 走这个方法
        if (view == null && context != context) {
            view = this.createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            this.checkOnClickListener((View)view, attrs);
        }

        return (View)view;
    }

在这里可以稍微总结一下,Factory2就是初始化View的入口。如何替换掉它呢?我们可以新建一个类实现Factory2接口 ,在AppCompatActivity onCreate 之前赋值给 LayoutInflater,这样就把入口替换成我们自定义实现的Factory2。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        //自定义类实现接口
        mSkinInflaterFactory = new SkinInflaterFactory(this);
        //把mSkinInflaterFactory 赋值给当前AppCompatActivity
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory);  
        super.onCreate(savedInstanceState);
    }

ThemeSkinning 源码分析

​ 接下来介绍一下 ThemeSkinning源码 工厂 SkinInflaterFactory入口

public class SkinInflaterFactory implements LayoutInflater.Factory2 {

    private static final String TAG = "SkinInflaterFactory";
    /**
     * 存储那些有皮肤更改需求的View及其对应的属性的集合
     */
    private Map<View, SkinItem> mSkinItemMap = new HashMap<>();
    private AppCompatActivity mAppCompatActivity;

    //这里为什么构造函数要传入AppCompatActivity ?
    public SkinInflaterFactory(AppCompatActivity appCompatActivity) {
        this.mAppCompatActivity = appCompatActivity;
    }

    @Override
    public View onCreateView(String s, Context context, AttributeSet attributeSet) {
        return null;
    }

    //这里的方法主要是收集View及其属性,View创建方法还是交给AppCompatDelegateImp实现Factory2接口的方法
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        
        Log.d(TAG, "onCreateView: init");
        boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        
        //通过获取AppCompatActivity.getDelegate() 实现类 来绘制View 
        AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
        View view = delegate.createView(parent, name, context, attrs);

        //添加TextView 方便后面更改字体
        if (view instanceof TextView && SkinConfig.isCanChangeFont()) {
            TextViewRepository.add(mAppCompatActivity, (TextView) view);
        }

        if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {
            if (view == null) {
                view = ViewProducer.createViewFromTag(context, name, attrs);
            }
            if (view == null) {
                return null;
            }
            parseSkinAttr(context, attrs, view);
        }
        return view;
    }

    /**
     * 收集换肤的 view
     * collect skin view
     */
    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
        List<SkinAttr> viewAttrs = new ArrayList<>();
        SkinL.i(TAG, "viewName:" + view.getClass().getSimpleName());
        //我们在 View 创建时,通过过滤 Attribute 属性,找到我们要标记的 View ,下面我们就把这些View的属性记下来
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attrName = attrs.getAttributeName(i);
            String attrValue = attrs.getAttributeValue(i);
            SkinL.i(TAG, "    AttributeName:" + attrName + "|attrValue:" + attrValue);
            //region  style
            //style theme
            if ("style".equals(attrName)) {
                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);
                    SkinAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);
                    SkinL.w(TAG, "    textColor in style is supported:" + "\n" +
                            "    resource id:" + textColorId + "\n" +
                            "    attrName:" + attrName + "\n" +
                            "    attrValue:" + attrValue + "\n" +
                            "    entryName:" + entryName + "\n" +
                            "    typeName:" + typeName);
                    if (skinAttr != null) {
                        viewAttrs.add(skinAttr);
                    }
                }
                if (backgroundId != -1) {
                    /**
                     * @color/item_tv_title_background
                     * backgroundId:2131034188
                     * entryName:item_tv_title_background   | resources name, eg:app_exit_btn_background
                     * typeName:color    |  type of the value , such as color or drawable
                     * attrName: style
                     */
                    String entryName = context.getResources().getResourceEntryName(backgroundId);
                    String typeName = context.getResources().getResourceTypeName(backgroundId);
                    SkinAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);
                    SkinL.w(TAG, "    background in style is supported:" + "\n" +
                            "    resource id:" + backgroundId + "\n" +
                            "    attrName:" + attrName + "\n" +
                            "    attrValue:" + attrValue + "\n" +
                            "    entryName:" + entryName + "\n" +
                            "    typeName:" + typeName);
                    if (skinAttr != null) {
                        viewAttrs.add(skinAttr);
                    }

                }
                a.recycle();
                continue;
            }
            //endregion
            //if attrValue is reference,eg:@color/red
            if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
                try {
                    //resource id
                    int id = Integer.parseInt(attrValue.substring(1));
                    if (id == 0) {
                        continue;
                    }
                    //entryName,eg:text_color_selector
                    String entryName = context.getResources().getResourceEntryName(id);
                    //typeName,eg:color、drawable
                    String typeName = context.getResources().getResourceTypeName(id);
                    SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
                    SkinL.w(TAG, "    " + attrName + " is supported:" + "\n" +
                            "    resource id:" + id + "\n" +
                            "    attrName:" + attrName + "\n" +
                            "    attrValue:" + attrValue + "\n" +
                            "    entryName:" + entryName + "\n" +
                            "    typeName:" + typeName
                    );
                    if (mSkinAttr != null) {
                        viewAttrs.add(mSkinAttr);
                    }
                } catch (NumberFormatException e) {
                    SkinL.e(TAG, e.toString());
                }
            }
        }
        if (!SkinListUtils.isEmpty(viewAttrs)) {
            SkinItem skinItem = new SkinItem();
            skinItem.view = view;
            skinItem.attrs = viewAttrs;
            mSkinItemMap.put(skinItem.view, skinItem);

            //重新打开应用,如果当前皮肤来自于外部或者是处于夜间模式就直接应用
            if (SkinManager.getInstance().isExternalSkin() ||
                    SkinManager.getInstance().isNightMode()) {
                skinItem.apply();
            }
        }
    }

    public void applySkin() {
        if (mSkinItemMap.isEmpty()) {
            return;
        }
        for (View view : mSkinItemMap.keySet()) {
            if (view == null) {
                continue;
            }
            mSkinItemMap.get(view).apply();
        }
    }

    /**
     * clear skin view
     */
    public void clean() {
        for (View view : mSkinItemMap.keySet()) {
            if (view == null) {
                continue;
            }
            mSkinItemMap.get(view).clean();
        }
        TextViewRepository.remove(mAppCompatActivity);
        mSkinItemMap.clear();
        mSkinItemMap = null;
    }

    //添加换肤的View
    private void addSkinView(SkinItem item) {
        if (mSkinItemMap.get(item.view) != null) {
            mSkinItemMap.get(item.view).attrs.addAll(item.attrs);
        } else {
            mSkinItemMap.put(item.view, item);
        }
    }

    //移除换肤的View
    public void removeSkinView(View view) {
        SkinL.i(TAG, "removeSkinView:" + view);
        SkinItem skinItem = mSkinItemMap.remove(view);
        if (skinItem != null) {
            SkinL.w(TAG, "removeSkinView from mSkinItemMap:" + skinItem.view);
        }
        if (SkinConfig.isCanChangeFont() && view instanceof TextView) {
            SkinL.e(TAG, "removeSkinView from TextViewRepository:" + view);
            TextViewRepository.remove(mAppCompatActivity, (TextView) view);
        }
    }

    /**
     * dynamicAddView(toolbar, "background", R.color.colorPrimaryDark);并添加到  mSkinItemMap 中
     * Dynamically add skin view
     * 动态添加皮肤视图
     * @param context        context
     * @param view           added view
     * @param attrName       attribute name   as "background"
     * @param attrValueResId resource id    as "R.color.colorPrimaryDark"
     */
    public void dynamicAddSkinEnableView(Context context, View view, String attrName, int attrValueResId) {

        //例如R.color.colorPrimaryDark 条目名称:colorPrimaryDark
        String entryName = context.getResources().getResourceEntryName(attrValueResId);
        //例如R.color.colorPrimaryDark 类型名称:color
        String typeName = context.getResources().getResourceTypeName(attrValueResId);
        Log.i("bbn", "dynamicAddSkinEnableView: "+entryName+"  "+typeName);
        SkinAttr mSkinAttr = AttrFactory.get(attrName, attrValueResId, entryName, typeName);
        SkinItem skinItem = new SkinItem();
        skinItem.view = view;
        List<SkinAttr> viewAttrs = new ArrayList<>();
        viewAttrs.add(mSkinAttr);
        skinItem.attrs = viewAttrs;
        skinItem.apply();
        addSkinView(skinItem);
    }

    /**
     * dynamic add skin view and it's attrs
     *
     * @param context context
     * @param view    view
     * @param attrs   attrs
     */
    public void dynamicAddSkinEnableView(Context context, View view, List<DynamicAttr> attrs) {
        List<SkinAttr> viewAttrs = new ArrayList<>();
        SkinItem skinItem = new SkinItem();
        skinItem.view = view;

        for (DynamicAttr dAttr : attrs) {
            int id = dAttr.refResId;
            String entryName = context.getResources().getResourceEntryName(id);
            String typeName = context.getResources().getResourceTypeName(id);
            SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName);
            viewAttrs.add(mSkinAttr);
        }

        skinItem.attrs = viewAttrs;
        skinItem.apply();
        addSkinView(skinItem);
    }

    // 动态添加TextView
    public void dynamicAddFontEnableView(Activity activity, TextView textView) {
        TextViewRepository.add(activity, textView);
    }
}

接着介绍 SkinBaseActivity 换肤的Activity 都需要继承它。实现ISkinUpdate和IDynamicNewView接口

public class SkinBaseActivity extends AppCompatActivity implements ISkinUpdate, IDynamicNewView {

    private SkinInflaterFactory mSkinInflaterFactory;

    private final static String TAG = "SkinBaseActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //初始化InflaterFactory,必须在onCreate方法中并且在调用super之前
        mSkinInflaterFactory = new SkinInflaterFactory(this);
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory);
        super.onCreate(savedInstanceState);
        changeStatusColor();
    }

    @Override
    protected void onResume() {
        super.onResume();
        //SkinManager 皮肤管理类绑定此 Activity 
        SkinManager.getInstance().attach(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        SkinManager.getInstance().detach(this);
        mSkinInflaterFactory.clean();
    }

    //ISkinUpdate 换肤监听回调
    @Override
    public void onThemeUpdate() {
        SkinL.i(TAG, "onThemeUpdate");
        mSkinInflaterFactory.applySkin();
        changeStatusColor();
    }

    public SkinInflaterFactory getInflaterFactory() {
        return mSkinInflaterFactory;
    }

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

    //IDynamicNewView 动态添加View 
    @Override
    public void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
        mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs);
    }

    @Override
    public void dynamicAddView(View view, String attrName, int attrValueResId) {
        mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
    }

    @Override
    public void dynamicAddFontView(TextView textView) {
        mSkinInflaterFactory.dynamicAddFontEnableView(this, textView);
    }

}

SkinManager 换肤管理类 主要负责绑定Activity ,加载皮肤包资源 ,通知Activity 换肤

public class SkinManager implements ISkinLoader {
    private static final String TAG = "SkinManager";

    //保存观察者
    private List<ISkinUpdate> mSkinObservers;

    @SuppressLint("StaticFieldLeak")
    private static volatile SkinManager mInstance;
    private Context context;
    private Resources mResources;
    private boolean isDefaultSkin = false;
    /**
     * skin package name
     */
    private String skinPackageName;

    private SkinManager() {

    }
    //单例模式
    public static SkinManager getInstance() {
        if (mInstance == null) {
            synchronized (SkinManager.class) {
                if (mInstance == null) {
                    mInstance = new SkinManager();
                }
            }
        }
        return mInstance;
    }

    //加载更换的皮肤,如果默认就不加载
    public void init(Context ctx) {
        context = ctx.getApplicationContext();

        //获取当前字体,并设置当前应用字字体
        TypefaceUtils.CURRENT_TYPEFACE = TypefaceUtils.getTypeface(context);

        //把皮肤复制到 /storage/emulated/0/Android/data/solid.ren.themeskinning/cache/skin 目录下
        setUpSkinFile(context);

        //判断是否是默认皮肤、夜间模式
        if (SkinConfig.isInNightMode(ctx)) {
            SkinManager.getInstance().nightMode();
        } else {
            String skin = SkinConfig.getCustomSkinPath(context);
            if (SkinConfig.isDefaultSkin(context)) {
                return;
            }
            loadSkin(skin, null);
        }
    }

    private void setUpSkinFile(Context context) {
        try {
            String[] skinFiles = context.getAssets().list(SkinConfig.SKIN_DIR_NAME);
            for (String fileName : skinFiles) {
                File file = new File(SkinFileUtils.getSkinDir(context), fileName);
                Log.d("file", "setUpSkinFile: "+SkinFileUtils.getSkinDir(context)+" "+fileName);
                if (!file.exists()) {
                    Log.d("file", "setUpSkinFile: file.exists()");
                    SkinFileUtils.copySkinAssetsToDir(context, fileName, SkinFileUtils.getSkinDir(context));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public int getColorPrimaryDark() {
        if (mResources != null) {
            int identify = mResources.getIdentifier("colorPrimaryDark", "color", skinPackageName);
            if (identify > 0) {
                return mResources.getColor(identify);
            }
        }
        return -1;
    }

    boolean isExternalSkin() {
        return !isDefaultSkin && mResources != null;
    }

    public String getCurSkinPackageName() {
        return skinPackageName;
    }

    public Resources getResources() {
        return mResources;
    }

    public void restoreDefaultTheme() {
        SkinConfig.saveSkinPath(context, SkinConfig.DEFAULT_SKIN);
        isDefaultSkin = true;
        SkinConfig.setNightMode(context, false);
        mResources = context.getResources();
        skinPackageName = context.getPackageName();
        notifySkinUpdate();
    }


    //绑定Activity 换肤的时候通知所有的Activity
    @Override
    public void attach(ISkinUpdate observer) {
        if (mSkinObservers == null) {
            mSkinObservers = new ArrayList<>();
        }
        if (!mSkinObservers.contains(observer)) {
            mSkinObservers.add(observer);
        }
    }

    //取消绑定
    @Override
    public void detach(ISkinUpdate observer) {
        if (mSkinObservers != null && mSkinObservers.contains(observer)) {
            mSkinObservers.remove(observer);
        }
    }

    //通知所有的Activity 更新换肤
    @Override
    public void notifySkinUpdate() {
        if (mSkinObservers != null) {
            for (ISkinUpdate observer : mSkinObservers) {
                observer.onThemeUpdate();
            }
        }
    }

    public boolean isNightMode() {
        return SkinConfig.isInNightMode(context);
    }

    //region Load skin or font


    /**
     * load skin form local
     * <p>
     * eg:theme.skin
     * </p>
     * 这里就是加载Apk 资源文件
     * @param skinName the name of skin(in assets/skin)
     * @param callback load Callback
     */
    @SuppressLint("StaticFieldLeak")
    public void loadSkin(String skinName, final SkinLoaderListener callback) {

        new AsyncTask<String, Void, Resources>() {

            @Override
            protected void onPreExecute() {
                if (callback != null) {
                    callback.onStart();
                }
            }

            @Override
            protected Resources doInBackground(String... params) {
                try {
                    if (params.length == 1) {
                        String skinPkgPath = SkinFileUtils.getSkinDir(context) + File.separator + params[0];
                        SkinL.i(TAG, "skinPackagePath:" + skinPkgPath);
                        File file = new File(skinPkgPath);
                        if (!file.exists()) {
                            return null;
                        }

                        PackageManager mPm = context.getPackageManager();
                        PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
                        skinPackageName = mInfo.packageName;

                        //每一个Resources管理的是一个文件路径下的资源。而我们要加载的外部资源肯定与我们在当前应用获取资源的路径不一样,
                        // 所以我们需要去重新创建一个加载外部文件的Resources对象。
                        //AssetManager 可以加载一个zip 格式的压缩包,而 Apk 文件不就是一个 压缩包吗。
                        // 我们通过反射的方法,拿到 AssetManager,加载 Apk 内部的资源,获取到 Resources 对象,这样再想办法,把 R文件里面保存的						ID获取到,这样就可以拿到对应的资源文件了。
                        AssetManager assetManager = AssetManager.class.newInstance();
                        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                        addAssetPath.invoke(assetManager, skinPkgPath);

                        //在Resources的构造函数中需要传入assetManager 对象,
                        // assetManager是真正管理资源的类,而且其构造函数被hidden,所以无法直接new,只能通过反射的方式去创建对象。
                        Resources superRes = context.getResources();
                        Resources skinResource = ResourcesCompat.getResources(assetManager, superRes.getDisplayMetrics(), 								  superRes.getConfiguration());
                        SkinConfig.saveSkinPath(context, params[0]);

                        isDefaultSkin = false;
                        return skinResource;
                    }
                    return null;
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }
            }

            @Override
            protected void onPostExecute(Resources result) {
                mResources = result;
                if (mResources != null) {
                    if (callback != null) {
                        callback.onSuccess();
                    }
                    SkinConfig.setNightMode(context, false);
                    notifySkinUpdate();
                } else {
                    isDefaultSkin = true;
                    if (callback != null) {
                        callback.onFailed("没有获取到资源");
                    }
                }
            }
        }.execute(skinName);
    }


    /**
     * load font
     *
     * @param fontName font name in assets/fonts
     */
    public void loadFont(String fontName) {
        Typeface tf = TypefaceUtils.createTypeface(context, fontName);
        TextViewRepository.applyFont(tf);
    }

    public void nightMode() {
        if (!isDefaultSkin) {
            restoreDefaultTheme();
        }
        SkinConfig.setNightMode(context, true);
        notifySkinUpdate();
    }
    //endregion

    //region Resource obtain
    //通过原来包名资源ID获取资源名称,通过资源名称获取现在皮肤包的资源ID,就可以实现换肤的操作
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public int getColor(int resId) {
        int originColor = ContextCompat.getColor(context, resId);
        if (mResources == null || isDefaultSkin) {
            return originColor;
        }

        String resName = context.getResources().getResourceEntryName(resId);

        int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
        int trueColor;
        if (trueResId == 0) {
            trueColor = originColor;
        } else {
            trueColor = mResources.getColor(trueResId);
        }
        return trueColor;
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public ColorStateList getNightColorStateList(int resId) {

        String resName = mResources.getResourceEntryName(resId);
        String resNameNight = resName + "_night";
        int nightResId = mResources.getIdentifier(resNameNight, "color", skinPackageName);
        if (nightResId == 0) {
            return ContextCompat.getColorStateList(context, resId);
        } else {
            return ContextCompat.getColorStateList(context, nightResId);
        }
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public int getNightColor(int resId) {
        String resName = mResources.getResourceEntryName(resId);
        String resNameNight = resName + "_night";
        int nightResId = mResources.getIdentifier(resNameNight, "color", skinPackageName);
        if (nightResId == 0) {
            return ContextCompat.getColor(context, resId);
        } else {
            return ContextCompat.getColor(context, nightResId);
        }
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public Drawable getNightDrawable(String resName) {

        String resNameNight = resName + "_night";

        int nightResId = mResources.getIdentifier(resNameNight, "drawable", skinPackageName);
        if (nightResId == 0) {
            nightResId = mResources.getIdentifier(resNameNight, "mipmap", skinPackageName);
        }
        Drawable color;
        if (nightResId == 0) {
            int resId = mResources.getIdentifier(resName, "drawable", skinPackageName);
            if (resId == 0) {
                resId = mResources.getIdentifier(resName, "mipmap", skinPackageName);
            }
            color = mResources.getDrawable(resId);
        } else {
            color = mResources.getDrawable(nightResId);
        }
        return color;
    }

    /**
     * get drawable from specific directory
     *
     * @param resId res id
     * @param dir   res directory
     * @return drawable
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public Drawable getDrawable(int resId, String dir) {
        Drawable originDrawable = ContextCompat.getDrawable(context, resId);
        if (mResources == null || isDefaultSkin) {
            return originDrawable;
        }
        String resName = context.getResources().getResourceEntryName(resId);
        int trueResId = mResources.getIdentifier(resName, dir, skinPackageName);
        Drawable trueDrawable;
        if (trueResId == 0) {
            trueDrawable = originDrawable;
        } else {
            if (android.os.Build.VERSION.SDK_INT < 22) {
                trueDrawable = mResources.getDrawable(trueResId);
            } else {
                trueDrawable = mResources.getDrawable(trueResId, null);
            }
        }
        return trueDrawable;
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public Drawable getDrawable(int resId) {

        Drawable originDrawable = ContextCompat.getDrawable(context, resId);
        if (mResources == null || isDefaultSkin) {
            return originDrawable;
        }
        String resName = context.getResources().getResourceEntryName(resId);
        int trueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
        Drawable trueDrawable;
        if (trueResId == 0) {
            trueResId = mResources.getIdentifier(resName, "mipmap", skinPackageName);
        }
        if (trueResId == 0) {
            trueDrawable = originDrawable;
        } else {
            if (android.os.Build.VERSION.SDK_INT < 22) {
                trueDrawable = mResources.getDrawable(trueResId);
            } else {
                trueDrawable = mResources.getDrawable(trueResId, null);
            }
        }
        return trueDrawable;
    }

    /**
     * 加载指定资源颜色drawable,转化为ColorStateList,保证selector类型的Color也能被转换。
     * 无皮肤包资源返回默认主题颜色
     * author:pinotao
     *
     * @param resId resources id
     * @return ColorStateList
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public ColorStateList getColorStateList(int resId) {
        boolean isExternalSkin = true;
        if (mResources == null || isDefaultSkin) {
            isExternalSkin = false;
        }

        String resName = context.getResources().getResourceEntryName(resId);
        if (isExternalSkin) {
            int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
            ColorStateList trueColorList;
            if (trueResId == 0) { // 如果皮肤包没有复写该资源,但是需要判断是否是ColorStateList

                return ContextCompat.getColorStateList(context, resId);
            } else {
                trueColorList = mResources.getColorStateList(trueResId);
                return trueColorList;
            }
        } else {
            return ContextCompat.getColorStateList(context, resId);
        }
    }
    //endregion
}

posted @ 2023-03-07 17:40  炸憨啪  阅读(56)  评论(0编辑  收藏  举报