Android换肤框架Debug 7.1.1源码一步步写_getresourceentryname mipmap notfound-程序员宅基地

技术标签: Android  

大家好,我是徐爱卿。博客地址:flutterall.com

这个SkinAPPDemo是很早的时候就写好的,今天才来总结,实在惭愧。–

成果

其实,Android换肤这个功能呢从v7包中谷歌就跟我们做了一个很好的示范。同时呢,谷歌也给我们提供了一个针对View去做自定义操作的接口。说了这么多,不如来点实际的。

本篇博客的的demo中的build.gradle配置是:

compileSdkVersion 25
buildToolsVersion "25.0.0"

然后进行debug源码时,也是基于Android7.1.1的源码进行的。后面一大波debug来袭,请留神。

这篇文章我们会从最简单的XML文件开始聊起。个人觉得知识点有以下:

  • 不断的debug到Android源码的内部能够加深我们对知识的了解
  • 不断的debug,可以知道那些报错的error信息生成的原因,以便于我们更好的解决问题
  • 学习Google的编码方式
  • 加深对LayoutInflater的理解
  • 看源码实现换肤

思考什么是换肤?

说白了,就是改变控件的背景以及颜色或者其本身的颜色。比如:更换TextView的字体颜色、背景颜色等等;在比如:更换LinearLayout的的背景以及颜色,等等。这些都属于换肤的范围。
###思考如何换肤?
两个切入点:

  • 方法一:view初始完成后,通过findViewById等方法拿到当前的控件,然后根据主题样式,设置其颜色、背景等。
  • 方法二:在view的构建初期,直接更改其颜色、背景等。

孰好孰坏,不言而喻。我呢,就要使用方法二。这个方法听起来不错,如何实现呢?其实,Google已经告诉我们了。天下文章一大抄,看你会抄不会抄。我们直接分析Google的实现逻辑,然后再写我们需要的逻辑。

有人问了,Android源码在哪里实现了?哥们别急,开讲了。

#引入
我们写一个简单的页面,里面就一个TextView,如下:

创建一个TextView

然后我们打印java Log.d(TAG, "tv instanceof AppCompatTextView ? -> "+(tv instanceof AppCompatTextView) +"");这句话,如下:(注意红色框中的,就可以了)

tv instanceof AppCompatTextView

看到结果不知道大家有没有些许疑问?在Android 7.1.1上运行的TextView竟然是AppCompatTextView的实例。我明明在XML中写的是TextView,在这里怎么就是AppCompatTextView的实例了呢?很明显,是在解析XML之后构建View对象的初期,看到是TextView标签直接使用AppCompatTextView构建这个对象。轮廓流程如下:
xml转化成View

我们关键看最后一步,看下如何“创建AppCompatTextView”,当我们知道了如何偶从一个XML文件变身为一个View对象后,我们就可以比葫芦画瓢,创建我们的自己的属性的View了。

分析LayoutInflater

从这里开始,我们全部使用debug结果一部部分析并且来验证了,免得空口说大话了。

我这里,创建一个SelectThemeActivity,继承自AppCompatActivity 我们先从最简单的 setContentView(R.layout.activity_select_theme);开始。

起初,执行的是startActivity(new Intent(this, SelectThemeActivity.class));],然后这个东东再向后调用ActivityManagerProxy#startActivity, ActivityManagerProxy是ActivityManager的一个远程代理,不用管它。然后通过ActivityThread的内部Handler类执行performLaunchActivity,最后调用Instrumentation#callActivityOnCreate(Activity activity, Bundle icicle)

这一块的流程如下:
从startActivity到init Factory
一定要记得下面这张图,这是一个关键点。
系统的Factory
这里注意一点layoutInflater.getFactory(),返回的是LayoutInflater的一个内部接口Factory

layoutInflater.getFactory()高能注意

在这里默认没有代码干预的情况下,我们不设置Factory的情况下,layoutInflater.getFactory()等于null,系统会自己创建一个Factory去处理XML到View的转换。但是!!!他这个。反之,如果我们设置了自己的Factory,那么系统就会走我们Factory的onCreateView,他会返回一个我们定制化的View。下面详细讲解。

Factory定义如下:

public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         * 
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         * 
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         * 
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

Factory

Factory是一个很强大的接口。当我们使用inflating一个XML布局时,可以使用这个类进行拦截解析到的XML中的标签属性-AttributeSet和上下文-Context,以及标签名称-name(例如:TextView)。然后我们根据这些属性可以创建对应的View,设置一些对应的属性。

比如:我读取到XML中的TextView标签,这时,我就创建一个AppCompatTextView对象,它的构造方法中就是我读取到的XML属性。然后,将构造好的View返回即可。

默认情况下,从context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)得到LayoutInflater,然后通过layoutInflater.getFactory()刚开始是null,然后执行LayoutInflaterCompat.setFactory(layoutInflater, this);方法。
看下这个方法。

  * Attach a custom Factory interface for creating views while using
     * this LayoutInflater. This must not be null, and can only be set once;
     * after setting, you can not change the factory.
     *
     * @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory)
     */
    public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
    
        IMPL.setFactory(inflater, factory);
    }

大致意识是:将一个自定义的Factory接口绑定到创建View的LayoutInflatr。这个接口的实现不能为空,同时只能设置一次(在代码中会有mFactorySet的boolean值(默认是false)标记是否已经设置过,如果重复设置,会抛异常)

在这里我们关注传入的LayoutInflaterFactory的实例,最终这个设置的LayoutInflaterFactory传入到哪里了呢?,我们向下debug,进入LayoutInflater中的下面:
LayoutInflater#setFactory2
给mFactory = mFactory2 = factory执行了,进行mFactory和mFactory2的赋值。

到这里走的路程,初始化好了LayoutInflater和LayoutInflaterFactory。

这里,我们就走完了SelectThemeActivity#onCreate中的super.onCreate(savedInstanceState);下面开始走setContentView(R.layout.activity_select_theme);

setContentView(int resId)

setContentView会走到LayoutInflate的下面这里:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }
			//在这里将Resource得到layout的XmlResourceParser对象
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

再向下就到了LayoutInflate重点:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            .....
            //将上面给我的XmlPullParser转换为对应的View的属性AttributeSet供View的构造方法或其他方法使用
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            ....
          try {
          if{
                ....
             } else {
             //默认布局会走到这里,Temp是XML文件的根布局
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
						...

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);
						....
						//添加解析到的根View
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
						....
					}

            } catch (XmlPullParserException e) {
               ....
            return result;
        }
    }

进入到createViewFromTag方法之中,会进入到LayoutInflate的View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)中。
到这里开始,我们开始学习源码中是如何使用Factory的。会走到下面这里:
LayoutInflate#createViewFromTag
这里的name传入的就是就是解析到的标签值LinearLayout。

@Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // First let the Activity's Factory try and inflate the view
        先试着进行解析布局
        final View view = callActivityOnCreateView(parent, name, context, attrs);
        if (view != null) {
            return view;
        }

        // If the Factory didn't handle it, let our createView() method try
        return createView(parent, name, context, attrs);
    }

很遗憾, callActivityOnCreateView返回的总是null:

@Override
    View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // On Honeycomb+, Activity's private inflater factory will handle calling its
        // onCreateView(...)
        return null;
    }

然后进入到下面的,createView(parent, name, context, attrs);中。高潮来了》》》》》,我期盼已久的看看Google源码是如何创建View的。

从XML到View的华丽转身

根据标签+属性创建对象

public 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 = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        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 it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

看到木有,它是拿标签名称进行switch的比较,是哪一个就进入到哪一个中进行创建View。
有人说啦,这里没有LinearLayout对应的switch啊。的确。最终返回null。

AppCompatViewInflater#createView并没有对布局进行创建对象

这里回到最初,由于Line769返回null,同时name值LinearLayout不包含".",进入到Line785onCreateView(parent, name, attrs)

二次解析布局标签

到这里,我们知道这个标签是LinearLayout了,那么开始创建这个对象了。问题来了,我们知道这个对象名称了,但是它属于哪个包名?如何创建呢?

根据标签名称创建对象

我们知道Android控件中的包名总共就那么几个:android.widget.]android.webkit.]android.app.],既然就这么几种,那么我干脆挨个用这些字符串进行如下拼接:
android.widget.LinearLayout]android.webkit.LinearLayout]android.app.LinearLayout],然后挨个创建对象,一旦创建成功即说明这个标签所在的包名是对的,返回这个对象即可。那么,从上面debug会进入到如下源码:
进入二次解析布局标签

sClassPrefixList的定义如下:

private static final String[] sClassPrefixList = {
    
        "android.widget.",
        "android.webkit.",
        "android.app."
    };

注意:是final的

创建Android布局标签对象

继续向下debug,进入到真正的创建Android布局标签对象的实现。在这个方法中,才是“android.widget.”包下的,LinearLayout、RelativeLayout等等的具体实现

创建LinearLayout

name=“LinearLayout”
prefix=“android.widget.”

下面分析下这段代码(下面的方法中去掉了一些无用代码):

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
    
//step1 :sConstructorMap是<标签名称:标签对象>的map,用来缓存对象的。第一次进入时,这个map中是空的。
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
    
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
    
//step2:在map缓存中没有找到对应的LinearLayout为key的对象,则创建。
            if (constructor == null) {
    
                // Class not found in the cache, see if it's real, and try to add it
  
//step3:【关键点,反射创建LinearLayout对象】,根据"prefix + name"值是"android.widget.LinearLayout"加载对应的字节码文件对象。
              clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                if (mFilter != null && clazz != null) {
    
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
    
                        failNotAllowed(name, prefix, attrs);
                    }
                }
//step4:获取LinearLayout的Constructor对象
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
//step5:缓存LinearLayout的Constructor对象
                sConstructorMap.put(name, constructor);
            } else {
    
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
    
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
    
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
    
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
    
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object[] args = mConstructorArgs;
            args[1] = attrs;
//step6:args的两个值分别是SelectThemeActivity,XmlBlock$Parser。到这里就调用了LinearLayout的两个参数的构造方法去实例化对象。至此,LinearLayout的实现也就是Android中的布局文件的实现全部完成。最后把创建的View给return即可。
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
    
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view;

        } 
                                      ......
    }

在这个方法中关键的步骤就是如何去实例化布局标签对象。这也是我们下面换肤的前提知识。

总结下根据标签+属性创建View的思路:

根据标签+属性创建View

#两个关键点:

  • 是否设置了Factory
  • Factory的onCreateView是否返回null

再让我们回到最初的地方:

23

        
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);
            }
//请注意下面?这个判断,系统肯定会走上面的mFactory2.onCreateView,
//默认系统的Factory返回的是null,
//所以系统会走下面自己的创建View的实现逻辑。

//如果我们在上面的流程图的第一步中设置了自己的Factory,那么系统
//会调用我们自己的Factory的createView的方法,这个时候,如果我们
//自己的Factory#onCreateView != null,那么就是返回我们的View了。
            if (view == null) {
    
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
    
                    if (-1 == name.indexOf('.')) {
    
                        view = onCreateView(parent, name, attrs);
                    } else {
    
                        view = createView(name, null, attrs);
                    }
                } finally {
    
                    mConstructorArgs[0] = lastContext;
                }
            }

 return view;

换肤开始

换肤代码思路:

换肤代码思路

我们通过我们view的属性的值white,拿到skin-apk中的white属性的skinResId,然后根据skinRes.getColor(skinResId)返回color,然后设置到我们的TextView上面。

step1 实现LayoutInflaterFactory接口,创建自己的Factory

public  class SkinActivity extends AppCompatActivity {
    

    protected LayoutInflaterFactoryImpl layoutInflaterFactory;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
        layoutInflaterFactory = new LayoutInflaterFactoryImpl();
        LayoutInflaterCompat.setFactory(getLayoutInflater(), layoutInflaterFactory);
        super.onCreate(savedInstanceState);
    }

}

看下运行结果:

创建LinearLayout

创建TextView

OK!没问题,每一个View的实现我们都可以拦截到,下一步开始拿取View的background、或者TextColor进行相应的更改。

step2 获取view需要换肤的属性

部分属性值

保存view的相关属性

public class ViewAttrs {
    


    public String attributeName, resourceEntryName, resourceTypeName;
    public int resId;


    public ViewAttrs(String attributeName, int resId, String resourceEntryName, String resourceTypeName) {
    
        this.attributeName = attributeName;
        this.resId = resId;
        this.resourceEntryName = resourceEntryName;
        this.resourceTypeName = resourceTypeName;
    }


}

View换肤时的操作

public class SkinView {
    

    private View view;
 
    private ArrayList<ViewAttrs> viewAttrses;

    public SkinView(View view, ArrayList<ViewAttrs> viewAttrses) {
    
        this.view = view;
        this.viewAttrses = viewAttrses;
    }

    //android:textColor = "@color/red_color"
    //android:background = "@mipmap/pic1"
    //android:background = "@drawable/selector"
    //android:background = "@color/blue_color"
    public void changeTheme() {
    
         //TODO 待实现的换肤代码
    }
}

onCreateView创建View后,读取view的属性值,并且保存

 /**
     * 解析本地view的属性,并保存该view
     * 解析:view的属性名称;view的属性值;view的background;view的resId
     * @param view
     * @param context
     * @param attrs
     */
    private void saveViewAttrs(View view, Context context, AttributeSet attrs) {
    
        //将view的每一种属性 以及对应的值放在list中
        ArrayList<ViewAttrs> viewAttrses = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
    
            String attributeName = attrs.getAttributeName(i);//background或者textColor
            String attributeValue = attrs.getAttributeValue(i);//拿到view的id。类似于@2131361811
            if(SkinConstans.BACKGROUND.equalsIgnoreCase(attributeName) || SkinConstans.TEXT_COLOR.equalsIgnoreCase(attributeName)){
    //暂且这样判断,后面会有优化后的代码
                int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
                String resourceTypeName = context.getResources().getResourceTypeName(resId);//background的mipmap或者drawable或者color等
                String resourceEntryName = context.getResources().getResourceEntryName(resId); //mipmap、drawable、color对应的值
                ViewAttrs viewAttrs = new ViewAttrs(attributeName, resId, resourceEntryName, resourceTypeName);
                viewAttrses.add(viewAttrs);
            }

        }
        if(viewAttrses.size() > 0){
    
            //保存需要换肤的view以及对应的属性
            SkinView skinView = new SkinView(view, viewAttrses);
            skinViews.add(skinView);
        }
    }

执行换肤时调用:

public void changeTheme(){
    
        for (int i = 0; i < skinViews.size(); i++) {
    
            skinViews.get(i).changeTheme();
        }
    }

step3 实现加载插件apk,并且拿到插件的资源对象

public void loadSkin(String skinPath) {
    
        //------------拿到skinPackageName----------
        skinPackageName = context.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
        //----------拿到skin中的Resource对象----------
        AssetManager assets = null;
        try {
    
            assets = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assets, skinPath);
        } catch (InstantiationException e) {
    
            e.printStackTrace();
        } catch (IllegalAccessException e) {
    
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
    
            e.printStackTrace();
        } catch (InvocationTargetException e) {
    
            e.printStackTrace();
        }
        skinRes = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    }

step 4 获取插件中的resId的值


    /**
     * @param resId
     * @return
     */
    public int getColor(int resId) {
    
        if (skinRes == null) {
    
            return resId;
        }
        //通过本地APP中的resId拿到本app对应的资源名称,然后再skin apk中找到该资源名称, 在根据skin中的资源名称 拿到对应的资源值
        String resourceName = context.getResources().getResourceName(resId);
        //String name, String defType, String skinPackageName  拿到skin包中的resId
        int skinResId = skinRes.getIdentifier(resourceName.substring(resourceName.indexOf("/") + 1), SkinConstans.COLOR, skinPackageName);
        if (skinResId == 0) {
    //说明在skin皮肤中没有找到对应的resId,则返回原本的resId
            return context.getResources().getColor(resId);
        }
        return skinRes.getColor(skinResId);
    }

    public Drawable getDrawable(int resId) {
    
        Drawable drawable = context.getResources().getDrawable(resId);
        if (skinRes == null) {
    
            return drawable;
        }

        String resourceName = context.getResources().getResourceName(resId);
        //String name, String defType, String skinPackageName  拿到skin包中的resId
        int skinResId = skinRes.getIdentifier(resourceName.substring(resourceName.indexOf("/") + 1), SkinConstans.DRAWABLE, skinPackageName);
        if (skinResId == 0) {
    //说明在skin皮肤中没有找到对应的resId,则返回原本的resId
            return drawable;
        }

        return skinRes.getDrawable(skinResId);
    }

step 5 SkinView中换肤

public void changeTheme() {
    
        for (int i = 0; i < viewAttrses.size(); i++) {
    
            ViewAttrs viewAttrs = viewAttrses.get(i);
            if (SkinConstans.TEXT_COLOR.equalsIgnoreCase(viewAttrs.attributeName)) {
    
                if (view instanceof TextView) {
    
                    //替换textColor
                    if (SkinConstans.COLOR.equalsIgnoreCase(viewAttrs.resourceTypeName)){
    
                        ((TextView) view).setTextColor(SkinManager.getInstance().getColor(viewAttrs.resId));
                    }
                }
            } else if (SkinConstans.BACKGROUND.equalsIgnoreCase(viewAttrs.attributeName)) {
    

                if (SkinConstans.DRAWABLE.equalsIgnoreCase(viewAttrs.resourceTypeName)) {
    

                    view.setBackgroundDrawable(SkinManager.getInstance().getDrawable(viewAttrs.resId));
                } else if (SkinConstans.COLOR.equalsIgnoreCase(viewAttrs.resourceTypeName)) {
    

                    view.setBackgroundColor(SkinManager.getInstance().getColor(viewAttrs.resId));
                } else if (SkinConstans.MIPMAP.equalsIgnoreCase(viewAttrs.resourceTypeName)) {
    

                }
            }
        }
    }

Paste_Image.png

代码跑起来

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/activity_main_color"
    android:orientation="vertical"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="xu.myapplication.MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/text_background_color"
        android:text="Hello World!"
        android:textColor="@color/text_color" />
    <Button
        android:text="换肤"
        android:onClick="changeTheme"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

color.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>
    <color name="text_color">#aadd00</color>
    <color name="text_background_color">#3F51B5</color>
    <color name="activity_main_color">#009977</color>
</resources>

MainActivity

public class MainActivity extends SkinActivity {
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


    public void changeTheme(View view){
    
        SkinManager.getInstance().initContext(this);
        ActivityCompat.requestPermissions(this,
                new String[]{
    Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        SkinManager.getInstance().loadSkin(Environment.getExternalStorageDirectory().getAbsolutePath()+"/skinplugin-debug.apk");
        layoutInflaterFactory.changeTheme();
    }
}

下面是skin-apk的color.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#3F51B5</color>
    <color name="text_color">#FF4081</color>
    <color name="text_background_color">#1199aa</color>
    <color name="activity_main_color">#aadd00</color>
</resources>

APP-color

skin-color

换肤结果

Activity的background和TextView的textColor都换了

代码优化:

优化这一块,有很多地方可以优化。比如:

  • 在saveViewAttrs(View view, Context context, AttributeSet attrs)方法中,将换肤进行抽象化处理。
private void saveViewAttrs(View view, Context context, AttributeSet attrs) {
    
        //将view的每一种属性 以及对应的值放在list中
        ArrayList<ViewAttrs> viewAttrses = new ArrayList<>();
        boolean skinEnable = true;
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
    
            String attributeName = attrs.getAttributeName(i);//background或者textColor
            String attributeValue = attrs.getAttributeValue(i);//拿到view的在R文件中的id。类似于@2131361811
            /*if(SkinConstant.BACKGROUND.equalsIgnoreCase(attributeName) || SkinConstant.TEXT_COLOR.equalsIgnoreCase(attributeName)){
                int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
                String resourceTypeName = context.getResources().getResourceTypeName(resId);//background的mipmap或者drawable或者color等
                String resourceEntryName = context.getResources().getResourceEntryName(resId); //mipmap、drawable、color对应的值
                ViewAttrs viewAttrs = new ViewAttrs(attributeName, resId, resourceEntryName, resourceTypeName);
                viewAttrses.add(viewAttrs);
            }*/
            if("skin".equalsIgnoreCase(attributeName)){
    
                //默认对所有控件换肤,但是如果属性中包含有[skin:skin=""],则表示不对该控件做换肤处理
                skinEnable = false;
                break;
            }
            if(!ViewAttrsFactory.contains(attributeName) || attributeValue.indexOf("@") < 0){
    
                continue;
            }

            int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
            String resourceTypeName = context.getResources().getResourceTypeName(resId);//background的mipmap或者drawable或者color等
            String resourceEntryName = context.getResources().getResourceEntryName(resId); //mipmap、drawable、color对应的值
            ViewAttrs viewAttrs = ViewAttrsFactory.createViewAttrs(attributeName, resId, resourceEntryName, resourceTypeName);
            if (viewAttrs != null) {
    
                viewAttrses.add(viewAttrs);

            }
        }
        if (skinEnable && viewAttrses.size() > 0) {
    
            //保存需要换肤的view以及对应的属性
            SkinView skinView = new SkinView(view, viewAttrses);
            skinViews.add(skinView);
            if(SkinManager.getInstance().isLoadSkinSuccess()){
    
                skinView.changeTheme();
            }
        }
    }

ViewAttrsFactory

public class ViewAttrsFactory {
    

    public static Map<String, ViewAttrs> viewAttrsMap = new HashMap<>();

    static {
    //添加支持换肤的属性
        viewAttrsMap.put(SkinConstant.TEXT_COLOR, new TextColorViewAttrs());
        viewAttrsMap.put(SkinConstant.BACKGROUND, new BackgroundViewAttrs());
        viewAttrsMap.put(SkinConstant.SRC, new BackgroundViewAttrs());
        viewAttrsMap.put(SkinConstant.MENU, new NavigationMenuAttrs());
    }

    public static ViewAttrs createViewAttrs(String attributeName, int resId, String resourceEntryName, String resourceTypeName) {
    
        if (viewAttrsMap.get(attributeName) != null) {
    
            ViewAttrs viewAttrs;
            if ((viewAttrs = viewAttrsMap.get(attributeName).clone()) != null) {
    

                viewAttrs.attributeName = attributeName;
                viewAttrs.resId = resId;
                viewAttrs.resourceEntryName = resourceEntryName;
                viewAttrs.resourceTypeName = resourceTypeName;
                return viewAttrs;
            }
        }
        return null;
    }


    public static boolean contains(String attributeName) {
    
        return attributeName != null && viewAttrsMap.get(attributeName) != null;
    }

}

更多优化,在我的GitHubSkinAppDemo,大家拉下来看下。这里就不在赘述了。

最终成果

总结

多多debug,多多益善!上面的我贴的debug的流程,还希望大家多多debug。反正我不记得我debug这个流程多少遍了。

感谢

谢谢大家最后坚持看到这里,期望大家多多fork,多多start。谢谢大家。

我的博客地址是 http://www.flutterall.com

谢谢

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/helloxjh/article/details/88424822

智能推荐

对NVMe SSD热插拔时,我需要注意什么?_presdet-程序员宅基地

文章浏览阅读2.2w次,点赞9次,收藏65次。NVMe SSD已经从实验阶段进入到大量业务部署时期,热插拔这个feature变得非常关键。最开始NVMe SSD只是以PCIe接口的形式出现,跟网卡一样放在背板的卡槽上固定,这种形态的NVMe还不适合热插拔。随着U.2接口(如下图)的推出,NVMe SSD可以直接如SATA/SAS硬盘一样放置在前面板,此时的NVMe SSD对热插拔的支持变得理所当然而且必须。_presdet

Java 调用 pytorch_5分钟!用Java实现目标检测 | PyTorch-程序员宅基地

文章浏览阅读2.6k次。鱼羊 编辑整理量子位 报道 | 公众号 QbitAI编者按:作为一个Java开发者,你是否曾为在PyTorch上部署模型而苦恼?这篇来自AWS软件工程师的投稿,结合实例,详细介绍了DJL这个为Java开发者设计的深度学习库:5分钟,你就能在PyTorch上,用Java实现目标检测。5分钟,用Java实现目标检测文 / 知乎用户@Lanking PyTorch在深度学习领域中的应用日趋广泛,得益于它..._java调用pytorch模型

backdoor-factory详细使用教程-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏13次。backdoor-factory简介backdoor-factory是一款后门构建工具。与其他工具不同的是,他不会增加软件的大小,而是利用代码缝隙进行注入,免杀效果更好。backdoor-factory下载kali预装的有坑,应该从github下载。运行命令: git clone https://github.com/secretsquirrel/the-backdoor-factory.gitbackdoor-factory常用命令-h 查看帮助..._backdoor-factory

线上java cpu占用过高问题排查_java 内存占用太高-程序员宅基地

文章浏览阅读6k次。1.首先查看占用内存占用排行top2.查看此进程中占用CPU较高的线程排行ps -mp PID(这里替换) -o THREAD,tid,time|sort -rn|head -n 20 从这里可以看出线程21154占用内存最高,我们继续。3.将此线程id转换成16进制,为我们接下来的操作做准备。printf &quot;%x\n&quot; tid4.用jstac..._java 内存占用太高

excel表格公式无效、不生效的解决方案及常见问题、常用函数-程序员宅基地

文章浏览阅读1.3w次。1、表格公式无效、不生效使用公式时碰到了一个问题,那就是公式明明已经编辑好了,但是在单元格里不生效,直接把公式显示出来了,网上资料说有4种原因,但是我4种都不是,是第5种原因,如下图:这种情况是由于单元格格式不是常规导致的,首先右键单击单元格-->设置单元格格式,如下图:然后把单元格格式修改为常规。然后双击该单元格,变成光标闪烁状态,敲回车,该公式就会生效了。..._excel里的or()为什么无效

oracle 回滚段快照过旧,关于快照过旧和无法扩展回滚段-程序员宅基地

文章浏览阅读814次。相信很多人都见过这样的错误,通常是一个程序运行了很久很久,突然报一个快照过旧的红色错误。那么什么叫作快照过旧呢?它是如何产生的呢?我们应当如何避免呢?快照过旧是指Oracle尝试读取一个过去时间点的表的数据,然而这些数据已经不在回滚段中存在了。为什么会有这种情况呢?这要牵涉到Oracle独特的多版本特性,它会通过回滚段来保证读数据的一致性:Oracle读取的数据,总是某一个时间点的表数据,无论后面..._快照过旧 回退段号42

随便推点

Go语言之interface详解_go interface-程序员宅基地

文章浏览阅读1.4w次,点赞15次,收藏55次。go语言interface详解,interface的使用场景,实现多态示例,_go interface

ScrollView中设置子控件填充满ScrollView_scrollview 允许子容器填充-程序员宅基地

文章浏览阅读3.1k次。之前写了一个水平的ScrollView,想在里面加一个LinearLayout,并填充满父控件,但是发现无论如何LinearLayout都不能填充满ScrollView,后来找到了方法Mark一下:只要在ScrollView中加上一个android:fillViewport="true"就解决了。可以看出ScrollView里面的子控件是自适应大小的。_scrollview 允许子容器填充

API设计:Swagger, Blueprint和RAML_swagger与raml的区别-程序员宅基地

文章浏览阅读2.6k次。SwaggerSwagger与RAML相比,RAML解决的问题是设计阶段的问题,而Swagger则是侧重解决现有API的文档问题,它们最大的不同是RAML需要单独维护一套文档,而Swagger则是通过一套反射机制从代码中生成文档,并且借助ajax可以直接在文档中对API进行交互。因为代码与文档是捆绑的所以在迭代代码的时候,就能方便的将文档也更新了。不会出现随着项目推移代码与文档不匹配的问题_swagger与raml的区别

损失函数——交叉熵损失函数_bp神经网络交叉熵loss曲线图-程序员宅基地

文章浏览阅读2.5k次。交叉熵代价函数(Cross-entropy cost function)是用来衡量人工神经网络(ANN)的预测值与实际值的一种方式。与二次代价函数相比,它能更有效地促进ANN的训练。在介绍交叉熵代价函数之前,本文先简要介绍二次代价函数,以及其存在的不足。一、二次代价函数的不足ANN的设计目的之一是为了使机器可以像人一样学习知识。人在学习分析新事物时,当发现自己犯的错误越大时,改正的力度就越大。比如投篮:当运动员发现自己的投篮方向离正确方向越远,那么他调整的投篮角度就应该越大,篮球就更容易投进篮筐。同理,_bp神经网络交叉熵loss曲线图

Systemverilog中时间单位以及相关系统函数_systemverilog 时间函数-程序员宅基地

文章浏览阅读1.2w次,点赞4次,收藏26次。在Systemverilog中有一些与时间相关的系统函数在TB打印log的时候会使用到,在打印log时间的时候,如果与我们预期的不一致,可以在这方面找原因。下面列出相关的系统函数$time$stime$realtime`timescale$printtimescale$time: 返回module 64bit 整数时间单位,这里的时间单位做一下说明,比如 `timescale 10ns/1ns , 时间单位就是10ns`timescale 10ns/1nsmodule test;_systemverilog 时间函数

html5游戏加入计时器,html5倒计时插件制作圆形计时器代码-程序员宅基地

文章浏览阅读285次。特效描述:html5倒计时插件 圆形计时器代码。计时器代码结构1. 引入CSS2. 引入JS3. HTML代码jQuery计时器插件TimeCircles演示1演示1(默认+美化)演示2(带控制)离2014年1月1日还有(2014年1月1日已过)倒计时10秒后结束页面开始时计时$(function(){$('#someTimer1').TimeCircles({time : {Days: {sho..._html5 圆形倒计时

推荐文章

热门文章

相关标签