Android 动态换肤框架原理

1.  Android 系统 PhoneWindow 源码阅读

 1.1.  Activity实例化 PhoneWindow

  Activity: 
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
 // Activity 中通过 PhoneWindow 加载布局,PhoneWindow AndrodStudio源码无法查看
// 上这里去看 http://androidxref.com/
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);
    }

1.2.  PhoneWindow 中 内部 DecorView

 private DecorView mDecor;
	// 
 int layoutResource;
 //  做一系列判断,去加载系统的 layout资源文件 
   if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
        }
		...... 
		// 把系统布局加入到 DecorView 中  
		// 系统布局 是一个 FrameLayout的 ViewGroup
		//  id 是 android.R.id.content  叫做 mContentParent
 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 
      // 自己调用setContenView 布局位置,自己的布局 Activity 

内部结构: 

2.  分析 ImageView 拦截

// 继承Activity,那么 返回 ImageView ,继承 AppCompatActivity 返回  AppCompatImageView

为什么?  在 AppCompatImageView 中 ImageView被替换成 AppCompatImageView, 看源码

2.1 .AppCompatActivity  源码   getDelegate

 @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
	    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }

2.2.  AppCompatDelegate 最终实例化的是 AppCompatDelegateImplV9

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (BuildCompat.isAtLeastN()) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

最终看 AppCompatDelegateImplV9 的 createView

核心 LayoutInflaterCompat.setFactory2(layoutInflater, this)   那么 this  实现 factory2接口  重写factory2接口方法

public static void setFactory2(
        @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
    IMPL.setFactory2(inflater, factory);
}

public interface Factory2 extends Factory {
    /**
     * Version of {@link #onCreateView(String, Context, AttributeSet)}
     * that also supplies the parent that the view created view will be
     * placed in.
     *
     * @param parent The parent that the created view will be placed
     * in; <em>note that this may be null</em>.
     * @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(View parent, String name, Context context, AttributeSet attrs);
}

 AppCompatDelegateImplV9 的  createView方法

@Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        if (mAppCompatViewInflater == null) {
            mAppCompatViewInflater = new AppCompatViewInflater();
        }

        boolean inheritContext = false;
        if (IS_PRE_LOLLIPOP) {
            inheritContext = (attrs instanceof XmlPullParser)
                    // If we have a XmlPullParser, we can detect where we are in the layout
                    ? ((XmlPullParser) attrs).getDepth() > 1
                    // Otherwise we have to use the old heuristic
                    : shouldInheritContext((ViewParent) parent);
        }

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
	
// 	LayoutInflaterCompat.setFactory2(layoutInflater, this); 
//  那么 this 就是  setFactory2(
   //         @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) 
   //      IMPL.setFactory2(inflater, factory) 的实现
// 
		
		   @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
     }
    }

AppCompatDelegateImplV9  中重写 factory2的 createView方法, 这里回调,等待实例化 factory2  调用 createView

 AppCompatDelegateImplV9 的  createView方法 内部 AppCompatViewInflater 调用  createView  控件 替换 如何下

 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);
			}
		}

   @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
		// 这里是单例
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

2.4.  那么什么时候  实例化 factory2  调用 createView,AppCompatDelegateImplV9  中重写 factory2的 createView方法 什么时候调用 ??  看  LayoutInflater 源码 

创建标签的时候 mFactory2.onCreateView, 那么 在 AppCompatViewInflater中的 createView被 调用,替换创建的标签,上面是钩子

    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) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
		// 开始解析代码 
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
	 // 解析 标签
	   rInflate(parser, root, inflaterContext, attrs, false);
	 }

	// 核心代码区
	 void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
			// 
			       final View view = createViewFromTag(parent, name, context, attrs);
			}
			
			
// 根据反射创建标签
	View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
			//  AppCompatViewInflater设置了 mFactory!=null 
			  try {
            View view;
            if (mFactory2 != null) {
			// 这里调用了 mFactory2.onCreateView 内部实现是 
			// AppCompatViewInflater 中 createView 方法实现 拦截,拦截View的 创建 
                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  
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
					// 表示自定义 View 
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

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


public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
			
			   Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
       // 从 构造器中 获取View,如果存在,那么直接获取 ,
	   // 不存在,通过反射创建 View ,存入 Map中 , 比如ImageView创建一次即可 
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                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);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                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);
                    }
                }
            }
			
			}

     LayoutInflater 中加载  来源, 是系统的一个服务,系统服务注册 , SystemServiceRegistry:

	// 获取系统服务的时候从 Map中获取即可  
		// 系统服务 SYSTEM_SERVICE_NAMES 保存到这个Map中  

			 registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
	
	
				  private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

动态换肤: 既然上面  AppCompatActivity 可以把ImageView 替换 AppCompatImageView,通过设置, 
那么我们也可以重写factory  来拦截View 的创建, 实现动态换肤恭喜

3  动态换肤原理:

 换肤原理:

任何一个apk中,资源文件  color , mip , drawable ....  
android:textColor="?android:colorPrimary"
android:textColor="@color/text_color"
android:background="@mipmap/btn"
只要是名称 res_name 相同 ,那么任何一个apk 生成的 id(索引值是相同的) ,那么我们只需要把资源放入 一个 skin1.apk中即可,和主apk res_name相同,图片名称相同,颜色 名称相同 在color.xml 下

然后读取主 apk 控件属性对应 的value 的 int值   ,去资源apk中 找到对应资源即可,替换主apk中资源

只要是名称 res_name 相同 ,那么任何一个apk 生成的 id(索引值是相同的),如何理解: 看图

主apk resourec资源文件:

皮肤skin.apk  resourec资源文件:

主apk 颜色资源文件

skin 换肤 apk 颜色资源文件

然后读取主 apk 控件属性对应 的value 的 int值   ,去资源apk中 找到对应资源即可

资源文件替换代码:

1. 首先 创建一个 skin.apk ,如图 

 2.  adb push  skinp\build\outputs\apk\debug\red.apk /sdcard  

测试 资源替换代码 如何 :

  /**
     * 测试皮肤替换 :  adb push  skinp\build\outputs\apk\debug\red.apk /sdcard
     * @param view
     */
    public void testSkin(View view) {
        try {
            // 加载系统下皮肤
            String skinPath= Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator +"red.apk";
            File file=new File(skinPath);
            if(!file.exists()){
                Log.e(Tag,"文件不存在");
                return;
            }
            AssetManager asset  = AssetManager.class.newInstance();
            // 读取 本地一个  .skin 里面资源
            Resources  superRes = getResources();
            // 添加本地下载好的资源皮肤    Native 层 c和 c++ 怎么搞得
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            method.setAccessible(true);
            // 执行反射方法
            method.invoke(asset,skinPath);
            mSkinResources=new Resources(asset,superRes.getDisplayMetrics(),superRes.getConfiguration());
            packageName= getPackageManager().getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES).packageName ;
            Log.e(Tag, "packageName:"+packageName);

            // 根据 包名, 资源文件名称 , 在 drawabel目录下 , 获取id 值 对应的  0x7f06000054
            int drawableId=  mSkinResources.getIdentifier("btn", "drawable",packageName);

            // 获取 颜色的 id值
            int colorId=  mSkinResources.getIdentifier("text_color","color",packageName);

            Log.e(Tag, "packageName:"+packageName + "   drawable:"+ drawableId);
            // 根据id值  获取到 skin.apk 中 drawable对象
//            Drawable drawable=ContextCompat.getDrawable(this,drawableId);  // 获取自己目录下

            Drawable drawable=mSkinResources.getDrawable(drawableId);

            // 根据id 值获取到颜色
            ColorStateList colorStateList =  mSkinResources.getColorStateList(colorId);


            // 修改控件
            showImg.setBackground(drawable);
            showText.setTextColor(colorStateList);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }

    }

综上  换肤框架实现 源码:


package mk.denganzhi.com.zhiwenku;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.LayoutInflaterCompat;
import android.support.v4.view.LayoutInflaterFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.AppCompatImageView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class MainActivity extends AppCompatActivity {


  String Tag="denganzhi";


    private static final List<String> mAttributes = new ArrayList<>();
    static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");

        mAttributes.add("skinTypeface");
    }

  List<SkinView> skinViews=new ArrayList<>();
  ImageView showImg=null;
  TextView showText=null;


    @Override
    protected void onCreate(Bundle savedInstanceState) {

        /**
         *  LayoutInflater LayoutInflater =
         (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         LayoutInflater  是一个系统服务, 单例
         */
          LayoutInflater layoutInflater=  LayoutInflater.from(this);


//        //  setFactory
//        LayoutInflaterCompat.setFactory(layoutInflater, new LayoutInflaterFactory() {
//            @Override
//            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//                Log.e("denganzhi1","创建View被拦截:"+name);
//
//
//                // 1. 创建View
//
//                // 2. 解析属性
//
//                // 3.  同意交给SkinManager管理
//
//                if(name.equals("ImageView")){
//                    TextView textView=new TextView(MainActivity.this);
//                    textView.setText("拦截");
//                    return textView;
//                }
//                return null;
//            }
//        });





        //  setFactory2
        LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(String name, Context context, AttributeSet attrs) {

                Log.e("denganzhi1","创建View被拦截1:"+name);
                return null;
            }

            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                // 反射 classLoader
                View view = createViewFromTag(name, context, attrs);
                // 自定义View
                if(null ==  view){
                    view = createView(name, context, attrs);
                }


                SkinView skinView=new SkinView();  // 控件集合
                skinView.setView(view);

                List<SkinAttr> skinAttrs=new ArrayList<>();   // 每一个控件存放属性的集合
                int attrLength= attrs.getAttributeCount();
                for (int index=0;index<attrLength; index++){
                    // 获取名称,值
                    String attrName = attrs.getAttributeName(index);
                    //  不需要换肤 属性值
                    if (!mAttributes.contains(attrName)) {
                        continue;
                    }
                    // 不符合 换肤条件
                    String attrValue = attrs.getAttributeValue(index);
                    if (attrValue.startsWith("#")) {
                        continue;
                    }
                    /**
                     *
                     * android:background="?android:colorPrimary"   使用系统颜色值      ?16843827
                     android:background="#000000"       不符合换肤条件
                     android:background="@mipmap/ic_launcher"   使用之定义    @2131361793
                     所有的 value 都会别转化为  int 值
                     */
               //  attrName:background  attValue:@2131361792

                    int resId = 0 ;
                    if (attrValue.startsWith("@") ||  attrValue.startsWith("?")) {
                        attrValue = attrValue.substring(1);
                        resId=   Integer.parseInt(attrValue);
                    }

                    if(resId==0){   //不符合条件
                        continue;
                    }


                    SkinAttr skinAttr =new SkinAttr();
                    skinAttr.setSkyType(attrName);
                //    skinAttr.setmResName(resName);
                    skinAttr.setmResId(resId);
                    skinAttrs.add(skinAttr);
                }
                 if(skinAttrs.size()>0 ){
                     skinView.setViewName(name);
                     skinView.setmAttrs(skinAttrs);  // 添加属性集合
                     skinViews.add(skinView);        // 添加View
                     Log.e(Tag,"添加的View:"+ name + "   skinView: "+ skinView );
                 }


//                if(name.equals("ImageView")){
//                    TextView textView=new TextView(MainActivity.this);
//                    textView.setText("拦截");
//                    return textView;
//                }

                return view;
            }
        });

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        showImg = findViewById(R.id.showImg);
        showText = findViewById(R.id.showText);


        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED)
        {

            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    110);
        } else
        {

        }

//        myImageView = findViewById(R.id.myImageView);
//        Log.e("denganzhi",myImageView.getClass().toString());
    }


    // 比如根据 @123456 获取 颜色图片 名称
    public  String getResName(String attrValue) {
        // 如果   android:background="#ffffff"   那么过滤掉,不换皮肤
        if (attrValue.startsWith("@") ||  attrValue.startsWith("?")) {
            attrValue = attrValue.substring(1);
            // 根据id 去插件包中找  资源
            int resId = Integer.parseInt(attrValue);

            // 获取资源类型
            String resourceTypeName =  getResources().getResourceTypeName(resId);
            Log.e(Tag,"resourceTypeName: " +resourceTypeName);

            //  通过id 获取资源名字

            return  getResources().getResourceEntryName(resId);

        }

        return  null;

    }


    // 比如根据 @123456 获取 颜色图片 名称   btn_bg
    public  String getResNamebyResId(int  resId) {
        // 如果   android:background="#ffffff"   那么过滤掉,不换皮肤
            //  通过id 获取资源名字
            return  getResources().getResourceEntryName(resId);
    }
    // 类型 mip 、color、drawable、layout.....
    public String  getResTypebyResId(int resId){
        // 获取资源类型
        String resourceTypeName =  getResources().getResourceTypeName(resId);
        return  resourceTypeName;
    }



    Resources  mSkinResources =null;
    String packageName=null;
    public void changeSkin(View view) {

        Log.e(Tag,"换肤List:"+skinViews);

        try {
            // 加载系统下皮肤
            String skinPath= Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator +"red.apk";
            File file=new File(skinPath);
            if(!file.exists()){
                Log.e(Tag,"文件不存在");
                return;
            }
            AssetManager asset  = AssetManager.class.newInstance();
            // 读取 本地一个  .skin 里面资源
            Resources  superRes = getResources();
            // 添加本地下载好的资源皮肤    Native 层 c和 c++ 怎么搞得
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            method.setAccessible(true);
            // 执行反射方法
            method.invoke(asset,skinPath);
            mSkinResources=new Resources(asset,superRes.getDisplayMetrics(),superRes.getConfiguration());
            // 获取资源文件的包名
            packageName= getPackageManager().getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES).packageName ;



            if(!TextUtils.isEmpty(packageName)  && mSkinResources!=null ){
                for(int i=0;i<skinViews.size();i++){
                    SkinView skinView= skinViews.get(i);
                    View viewSkin= skinView.getView();
                    List<SkinAttr> skinAttrs= skinView.getmAttrs();
                    for (int j=0;j<skinAttrs.size();j++){

                        SkinAttr skinAttr= skinAttrs.get(j);
                        String skyType= skinAttr.getSkyType();
                        int mResId= skinAttr.getmResId();

                        String resName= getResNamebyResId(mResId);
                        String resType = getResTypebyResId(mResId);


                      if(TextUtils.isEmpty(resName)){  // 没有找到
                        continue;
                      }
                        // attrName:textColor  attValue:@2130968660  resName:text_color
                        // attrName:textColor  attValue:?16843827   resName:colorPrimary


                        Log.e(Tag, "packageName:"+packageName+"  resName:"+resName  + "   resType:"+ resType);

                        // 需要换肤的背景  background
                        if(skyType.equals("background")){

                             // 背景可以是图片  也可能是颜色
                            if(resType.equals("color")){
                                int background=  mSkinResources.getColor(mResId);
                                viewSkin.setBackgroundColor((Integer) background);
                            }else{
                                Drawable drawable= getDrawableByName(resName);
                                ImageView imageView= (ImageView) viewSkin;
                                imageView.setBackground(drawable);
                            }

                         // 需要换肤的 textcolor
                        }else if(skyType.equals("textColor")){

                            //     int colorId=  mSkinResources.getIdentifier("text_color","color",packageName);
                            //        ColorStateList colorStateList=  mSkinResources.getColorStateList(resId);
                            ColorStateList colorStateList=  getColorByName(resName);
                            TextView textView= (TextView)viewSkin;
                            textView.setTextColor(colorStateList);
                        }else if(skyType.equals("src")){

                        }else if(skyType.equals("skinTypeface")){

                             // 如何修改字体,思路

                            // 可以判断这里是 viewSkin 是TextView Button 然后设置  ,  第二个  参数传递不同路径即可
                            //  textView.setTypeface( Typeface.createFromAsset(this.getAssets(), "fonts/CodeBold.ttf"));
                        }
                    }
                }
            }

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }

  

    public ColorStateList getColorByName(String resName){
        int resId= mSkinResources.getIdentifier(resName,"color",packageName);
        ColorStateList colorStateList=  mSkinResources.getColorStateList(resId);
        Log.e(Tag,"colorId:"+resId);
        return  colorStateList;
    }

    public void restoreSkin(View view)  {


    }


    private static final String[] mClassPrefixlist = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };
    private View createViewFromTag(String name, Context context, AttributeSet attrs) {
        //包含自定义控件
        if (-1 != name.indexOf(".")) {
            return null;
        }
        //
        View view = null;
        for (int i = 0; i < mClassPrefixlist.length; i++) {
            view = createView(mClassPrefixlist[i] + name, context, attrs);
            if(null != view){
                break;
            }
        }
        return view;
    }
    private static final Class[] mConstructorSignature =
            new Class[]{Context.class, AttributeSet.class};
    private static final HashMap<String, Constructor<? extends View>> mConstructor =
            new HashMap<String, Constructor<? extends View>>();
    private View createView(String name, Context context, AttributeSet attrs) {
        Constructor<? extends View> constructor = mConstructor.get(name);
        if (constructor == null) {
            try {
                //通过全类名获取class
                Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
                //获取构造方法
                constructor = aClass.getConstructor(mConstructorSignature);
                mConstructor.put(name, constructor);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (null != constructor) {
            try {
                return constructor.newInstance(context, attrs);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }


}

 SkinView.java

package mk.denganzhi.com.zhiwenku;

import android.view.View;

import java.util.List;

/**
 * Created by Administrator on 2020/5/24.
 */

public class SkinView {

    private View view;
    private String viewName;

    private List<SkinAttr> mAttrs;

    public View getView() {
        return view;
    }

    public void setView(View view) {
        this.view = view;
    }

    public List<SkinAttr> getmAttrs() {
        return mAttrs;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public void setmAttrs(List<SkinAttr> mAttrs) {
        this.mAttrs = mAttrs;
    }

    @Override
    public String toString() {
        return "SkinView{" +
                "view=" + view +
                ", viewName='" + viewName + '\'' +
                ", mAttrs=" + mAttrs +
                '}';
    }
}
SkinAttr.java

public class SkinAttr {

    private String mResName;
    private String skyType;
    private  int mResId;




    public String getmResName() {
        return mResName;
    }

    public void setmResName(String mResName) {
        this.mResName = mResName;
    }

    public String getSkyType() {
        return skyType;
    }

    public void setSkyType(String skyType) {
        this.skyType = skyType;
    }

    public int getmResId() {
        return mResId;
    }

    public void setmResId(int mResId) {
        this.mResId = mResId;
    }

    @Override
    public String toString() {
        return "SkinAttr{" +
                "mResName='" + mResName + '\'' +
                ", skyType='" + skyType + '\'' +
                ", mResId=" + mResId +
                '}';
    }
}

源码地址:https://download.csdn.net/download/dreams_deng/12454900

2023.6.17重新理解;

1.  看源码, 

【大概原理:Android动态换肤框架-换肤原理 - 简书

-- 从 setContentView(R.layout.activity_main); 点入:
AppCompatDelegateImpl.java

 public void setContentView(int resId) {
               LayoutInflater.from(mContext).inflate(resId, contentParent);
   }
  
LayoutInflater.java
      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
	  //Resources 可以理解为xml解析器
	   final Resources res = getContext().getResources();
	  }
	  
	  public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
	  //获取节点name 
	     final String name = parser.getName();
		 //创建View通过工厂或者
		     final View temp = createViewFromTag(root, name, inflaterContext, attrs);
			   root.addView(temp, params);
	  }
	  
	    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
			//如果有 mFactory2 工厂, 通过 mFactory2 工厂创建
		   if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
				// 	//如果有 mFactory 工厂, 通过 mFactory 工厂创建
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
			//如果没有工厂
			 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;
                }
            }
			}
			
			
			
 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
			//获取控件的.class类
			                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
						//通过放射获取构造函数
			  constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
				//创建对象! 
				            final View view = constructor.newInstance(args);
	}



	    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
			//获取控件的.class类
			                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
						//通过放射获取构造函数
			  constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
				//创建对象! 
				            final View view = constructor.newInstance(args);
			}

 大概原理就是系统 解析xml控件,createViewFromTag()的时候, 如果mFactory2不能与空,使用mFactory2 来创建控件,那么在app的onCreate中可以重写 Factory2, 

创建系统控件,自定义控件, 拦截获取控件的属性保存到一个集合中


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflater layoutInflater=  LayoutInflater.from(this);
        //  setFactory2
        LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(String name, Context context, AttributeSet attrs) {

                Log.e(Tag ,"创建View被拦截1:"+name);
                return null;
            }

            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                // 反射 classLoader
                View view = createViewFromTag(name, context, attrs);
                // 自定义View
                if(null ==  view){
                    view = createView(name, context, attrs);
                }

                Log.e(Tag ,"onCreateView:"+name);


//                SkinView skinView=new SkinView();  // 控件集合
//                skinView.setView(view);

      //          List<SkinAttr> skinAttrs=new ArrayList<>();   // 每一个控件存放属性的集合
                int attrLength= attrs.getAttributeCount();
                for (int index=0;index<attrLength; index++){
                    // 获取名称,值
                    String attrName = attrs.getAttributeName(index);
                    //  不需要换肤 属性值
                    if (!mAttributes.contains(attrName)) {
                        continue;
                    }
                    // 不符合 换肤条件
                    String attrValue = attrs.getAttributeValue(index);
                    if (attrValue.startsWith("#")) {
                        continue;
                    }
                    /**
                     *
                     * android:background="?android:colorPrimary"   使用系统颜色值      ?16843827
                     android:background="#000000"       不符合换肤条件
                     android:background="@mipmap/ic_launcher"   使用之定义    @2131361793
                     所有的 value 都会别转化为  int 值
                     */
                    //  attrName:background  attValue:@2131361792

                    int resId = 0 ;
                    if (attrValue.startsWith("@") ||  attrValue.startsWith("?")) {
                        Log.e("TAG", attrValue);
                        String  newattrValue = attrValue.substring(1);
                        resId=    Integer.parseInt(newattrValue);
                        //这个值就是 android.R.color.colorPrimary 的id

                      // int color=  getResources().getColor(resId);

                        Log.e("TAG",attrName+"   "+ attrValue +"   "+  Integer.toHexString(resId) +"     "   );

                        //         <attr name="colorPrimary" format="color" />
                        //

                        if(attrValue.startsWith("@")){
                            // ff6200ee  直接获取默认颜色值
                            Log.e("TAG", Integer.toHexString(getResources().getColor(resId)) +"     "   );
                            // type: color  name:   black + 包名,获取插件中的颜色
                            String resourceTypeName =getResources().getResourceTypeName(resId);
                            String resourceEntryName  = getResources().getResourceEntryName(resId);
                            Log.e("TAG","@:  "+resourceTypeName+ "   "+ resourceEntryName);
                        }


                        // 需要通过如下方式获取颜色值:
                        // android:textColor="?android:colorPrimary"
                        // resId=android:colorPrimary attr.xml 的id
                        // val值是:ff6200ee
                        if(attrValue.startsWith("?")){
                             int[] attrs1=new int[]{resId};
                            int[] resIds = new int[attrs1.length];
                            TypedArray a = context.obtainStyledAttributes(attrs1);
                            for (int i = 0; i < attrs1.length; i++) {
                                resIds[i] = a.getResourceId(i, 0);
                                Log.e("TAG",   "     "   + Integer.toHexString(getResources().getColor( resIds[i]))   + "   "  );
                            }
                            a.recycle();

                            String resourceTypeName =getResources().getResourceTypeName( resIds[0] );
                            String resourceEntryName  = getResources().getResourceEntryName(resIds[0]);
                            //  ?:  color   purple_500
                            Log.e("TAG","?:  "+resourceTypeName+ "   "+ resourceEntryName);
                        }


                        if(attrName.equals("textColor")){
                         //   int colorId= getResources().getColor(resId);
                          //  Log.e("TAG", "hex:" +  Integer.toHexString(colorId));
                        }else{

                        }
                    }

                    if(resId==0){   //不符合条件
                        continue;
                    }


//                    SkinAttr skinAttr =new SkinAttr();
//                    skinAttr.setSkyType(attrName);
//                    //    skinAttr.setmResName(resName);
//                    skinAttr.setmResId(resId);
//                    skinAttrs.add(skinAttr);
                }
          //      if(skinAttrs.size()>0 ){
//                    skinView.setViewName(name);
//                    skinView.setmAttrs(skinAttrs);  // 添加属性集合
//                    skinViews.add(skinView);        // 添加View
//                    Log.e(Tag,"添加的View:"+ name + "   skinView: "+ skinView );
           //     }


//                if(name.equals("ImageView")){
//                    TextView textView=new TextView(MainActivity.this);
//                    textView.setText("拦截");
//                    return textView;
//                }

                return view;
            }
        });
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageView = findViewById(R.id.imageView);
        textView = findViewById(R.id.textView);
        test_color2 = findViewById(R.id.test_color2);

        test_color2.setTextColor(getResources().getColor(R.color.test_color));
}

}

 通过上面代码采集完毕,保存起来

1. setFactory2 设置,采集系统控件需要换肤属性   
 
    装起来:  
	 key:控件 
	    kev-value: 属性-app属性值! 
	
	   private List<SkinView> skinViews = new ArrayList<>();

        View view;   比如textView  
        List<SkinPain> skinPains;   

   static class SkinPain {
        String attributeName;   //比如textcolor, 
        int resId;   // value值 

        public SkinPain(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }

有一个东西:可以读取apk皮肤包的 drawable, string, 给app用

博客:(2条消息) Android应用程序插件化研究之AssertManager_weixin_34159110的博客-CSDN博客

 上面已经获取到了每个控件属性值和 控件属性名

  根据值,获取对应的 

resourceTypeName、resourceEntryName
      
   /**
                     *
                     * android:background="?android:colorPrimary"   使用系统颜色值      ?16843827
                     android:background="#000000"       不符合换肤条件
                     android:background="@mipmap/ic_launcher"   使用之定义    @2131361793
                     所有的 value 都会别转化为  int 值
                     */
                    //  attrName:background  attValue:@2131361792

                    int resId = 0 ;
                    if (attrValue.startsWith("@") ||  attrValue.startsWith("?")) {
                        Log.e("TAG", attrValue);
                        String  newattrValue = attrValue.substring(1);
                        resId=    Integer.parseInt(newattrValue);
                        //这个值就是 android.R.color.colorPrimary 的id

                      // int color=  getResources().getColor(resId);

                        Log.e("TAG",attrName+"   "+ attrValue +"   "+  Integer.toHexString(resId) +"     "   );

                        //         <attr name="colorPrimary" format="color" />
                        //


 if(attrValue.startsWith("@")){
                            // ff6200ee  直接获取默认颜色值
                            Log.e("TAG", Integer.toHexString(getResources().getColor(resId)) +"     "   );
                            // type: color  name:   black + 包名,获取插件中的颜色
                            String resourceTypeName =getResources().getResourceTypeName(resId);
                            String resourceEntryName  = getResources().getResourceEntryName(resId);
                            Log.e("TAG","@:  "+resourceTypeName+ "   "+ resourceEntryName);
                        }


                        // 需要通过如下方式获取颜色值:
                        // android:textColor="?android:colorPrimary"
                        // resId=android:colorPrimary attr.xml 的id
                        // val值是:ff6200ee
                        if(attrValue.startsWith("?")){
                             int[] attrs1=new int[]{resId};
                            int[] resIds = new int[attrs1.length];
                            TypedArray a = context.obtainStyledAttributes(attrs1);
                            for (int i = 0; i < attrs1.length; i++) {
                                resIds[i] = a.getResourceId(i, 0);
                                Log.e("TAG",   "     "   + Integer.toHexString(getResources().getColor( resIds[i]))   + "   "  );
                            }
                            a.recycle();

                            String resourceTypeName =getResources().getResourceTypeName( resIds[0] );
                            String resourceEntryName  = getResources().getResourceEntryName(resIds[0]);
                            //  ?:  color   purple_500
                            Log.e("TAG","?:  "+resourceTypeName+ "   "+ resourceEntryName);
                        }

根据上一篇

------------------------------------------------------------------------------------------------------
1. 通过分析上面源码,自己写factory2, 创建view, 把View和需要换肤的属性保存起来

①. 如果有多个activity如何解决:     application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
 所有的actiivty创建以后都会走这里个方法

 走这个方法的时候,设置factory2,那么就可以获取到所有属性了


 ②.
        try {
            //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
            //如设置过抛出一次
            //设置 mFactorySet 标签为false
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }




2.  设置上面属性以后,获取到所有需要替换控件的 集合,然后走,   使用在 factory2中采集所有控件属性可以替换的
   setContentView(R.layout.activity_main); 方法!

  ***** 总结:



 替换东西如下;

====0. background(颜色,图片),src,drawableLeft,drawableTop,drawableRight,drawableBottom,textColor

如何替换:
  主apk和资源apk
			根据 android:background="@mipmap/btn"
			drawable/bnt找到图片id , 根据id getResources获取图片,


 写的博客; https://blog.csdn.net/dreams_deng/article/details/106320048



====1. 状态栏实现逻辑 : SkinThemeUtils.updateStatusBarColor

 状态栏颜色;
    如果配置           R.attr.colorPrimaryDark 用这个
	没有配置用系统的:  android.R.attr.statusBarColor, android.R.attr
            .navigationBarColor

			可以自己设置: getWindow().setStatusBarColor(SkinResources.getInstance().getColor(statusBarColorResId[0]));


			 android:textColor="?android:colorPrimary"
			                //对应三个值:
                        //resName: btn
                        // resType: drawable
                        // resId: R.id.btn对用的R文件的值
						// value值是 R.android.colorPrimary的R 值,
						// 然后在通过这个id获取颜色,
						//          ColorStateList colorStateList =  mSkinResources.getColorStateList(colorId);
						//


====2. 字体换肤

   如何实现:
        app中:  <string name="typeface"/>
		皮肤包中:<string name="typeface">font/global.ttf</string>
		读取皮肤包的val,
		根据:Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
		skinTypefacePath 就是  font/global.ttf
		如果是app包中:Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
		设置给textview即可



 颜色:
mSkinResources.getColor(skinId)
 多个颜色:
mSkinResources.getColorStateList(skinId);
  图片:
mSkinResources.getDrawable(skinId);
 字体:
Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);


1. setFactory2 设置,采集系统控件需要换肤属性

    装起来:
	 key:控件
	    kev-value: 属性-app属性值!

	   private List<SkinView> skinViews = new ArrayList<>();

        View view;   比如textView
        List<SkinPain> skinPains;

   static class SkinPain {
        String attributeName;   //比如textcolor,
        int resId;   // value值

        public SkinPain(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }
2.  skinViews 获取完毕,下载皮肤apk

3.  开始替换皮肤



==== 3. recycleview 中的item 替换字体

=== 4.  自定义view实现 ,自定义属性
  定义一个接口, public interface SkinViewSupport , 自定义控件实现,  换肤的时候调用一下方法,在apk包读取对应的属性,替换即可



 上面缺点:setFactory2如果android改源码了,无法实现了!!

  如何实现:
  1.把皮肤app包放在assert下,直接去读取app皮肤包,替换!!


************************************************************************************************************************************************************************************



  Android9.0 直接hide 调了一些api,不是所有,屏蔽放射,
  把 api 放入黑名单中,不可以放射!!



 /****
     * 夜间模式和日间模式实现:
     *      夜间读取: values-night 颜色值
     *      日间: 读取values 中的值
     */

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值