文章目录

    • 知识总览
    • 认识setFactory
    • 获取任意一个apk压缩文件的`Resource`对象
      • 1、如何创建自定义的Resource实例
      • 2、如何知道当前属性值在所在Resource中的id
    • 参考文章

知识总览

android主题换肤通常借助LayoutInflater#setFactory实现换肤。

换肤步骤:

  1. 通过解析外部的apk压缩文件,创建自定义的Resource对象去访问apk压缩文件的资源。
  2. 借助LayoutInfater#setFactoy,将步骤(1)中的资源应用到View的创建过程当中。

认识setFactory

平常设置或者获取一个View时,用的较多的是setContentViewLayoutInflater#inflatesetContentView内部也是通过调用LayoutInflater#inflate实现(具体调用在AppCompatViewInflater#setContentView(ind resId)中)。

通过LayoutInflater#inflate可以将xml布局文件解析为所需要的View,通过分析LayoutInflate#inflate源码,可以看到.xml布局文件在解析的过程中会调用LayoutInflater#rInflate,随后会通过调用LayoutInflater#createViewFromTag来创建View。这里推荐《遇见LayoutInflater&Factory》
下面一起看看View的创建过程LayoutInflate#createViewFormTag:

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;            if (mFactory2 != null) {            //根据attrs信息,通过mFactory2创建View                view = mFactory2.onCreateView(parent, name, context, attrs);            } else if (mFactory != null) {            //根据attrs信息,通过mFactory创建View                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 {                    if (-1 == name.indexOf('.')) {                    //创建Android原生的View(android.view包下面的view)                        view = onCreateView(parent, name, attrs);                    } else {                    //创建自定义View或者依赖包中的View(xml中声明的是全路径)                        view = createView(name, null, attrs);                    }                } 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;        }    }

从上述源码中可以看出View的创建过程中,会首先找Factory2#onCreateViewFactory#onCreateView进行创建,然后走默认的创建流程。所以,我们可以在此处创建自定义的Factory2Factory,并将自定义的Factory2Factory对象添加到LayoutInflater对象当中,来对View的创建进行干预,LayoutInflate也提供了相关的API供我们添加自己的ViewFactory

例如:下面我们通过设置LayoutInflaterFactory来,将视图中的Button转换为TextView

@Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {            @Override            public View onCreateView(String name, Context context, AttributeSet attrs) {                for (int i = 0; i < attrs.getAttributeCount(); i ++){                    String attrName = attrs.getAttributeName(i);                    String attrValue = attrs.getAttributeValue(i);                    Log.i(TAG, String.format("name = %s, attrName = %s, attrValue= %s", name, attrName, attrValue));                }                TextView textView = null;                if (name.equals("Button")){                     textView = new TextView(context, attrs);                }                return textView;            }        });        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_theme_change);    }

让后启动Activity后,视图中的Button都转化成了TextView,并且能看到输出:

name = Button, attrName = id, attrValue= @2131230758name = Button, attrName = background, attrValue= @2131034152name = Button, attrName = layout_width, attrValue= -2name = Button, attrName = layout_height, attrValue= -2name = Button, attrName = id, attrValue= @2131230757name = Button, attrName = background, attrValue= @2131034150name = Button, attrName = layout_width, attrValue= -2name = Button, attrName = layout_height, attrValue= -2

获取任意一个apk压缩文件的Resource对象

上述过程已经提供了更改View类型以及属性的方式,下面我们见介绍如何获取一个apk压缩文件中的res资源。

我们通常通过Context#getSource()获取res目录下的资源,Context#getAssets()(想当于Context#getSource().getAssets())获取asset目录下的资源。所以要获取一个apk压缩文件的资源文件,创建对应该压缩文件的Resource实例,然后通过这个实例获取压缩文件中的资源信息。
比如,新创建的的Resource实例为mResource,则可以使用mResource.getColor(colorId),来获取实例内colorId所对应的颜色。

那么接下来的问题分为两步:

1、如何创建自定义的Resource实例

Resource的构造函数Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)了解到,需要获取app外部apk文件资源的Resource对象,首先需要创建对应的AssetManager对象。

public final class AssetManager implements AutoCloseable {    /**     * Create a new AssetManager containing only the basic system assets.     * Applications will not generally use this method, instead retrieving the     * appropriate asset manager with {@link Resources#getAssets}.    Not for     * use by applications.     * {@hide}     */    public AssetManager() {        synchronized (this) {            if (DEBUG_REFS) {                mNumRefs = 0;                incRefsLocked(this.hashCode());            }            init(false);            if (localLOGV) Log.v(TAG, "New asset manager: " + this);            ensureSystemAssets();        }    }     /**     * Add an additional set of assets to the asset manager.  This can be     * either a directory or ZIP file.  Not for use by applications.  Returns     * the cookie of the added asset, or 0 on failure.     * {@hide}     */     //添加额外的asset路径    public final int addAssetPath(String path) {        synchronized (this) {            int res = addAssetPathNative(path);            if (mStringBlocks != null) {                makeStringBlocks(mStringBlocks);            }            return res;        }    }

所以通过反射可以创建对应的AssertManager,进而创建出对应的Resource实例,代码如下:

private final static Resources loadTheme(String skinPackageName, Context context){        String skinPackagePath = Environment.getExternalStorageDirectory() + "/" + skinPackageName;        File file = new File(skinPackagePath);        Resources skinResource = null;        if (!file.exists()) {            return skinResource;        }        try {            //创建AssetManager实例            AssetManager assetManager = AssetManager.class.newInstance();            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);            addAssetPath.invoke(assetManager, skinPackagePath);            //构建皮肤资源Resource实例            Resources superRes = context.getResources();            skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());        } catch (Exception e) {            skinResource = null;        }        return skinResource;    }

2、如何知道当前属性值在所在Resource中的id

Resource的源码中,可以发现

public class Resources {    /**     * 通过给的资源名称,类型和包名返回一个资源的标识id。     * @param name 资源的描述名称     * @param defType 资源的类型名称     * @param defPackage 包名     *      * @return 返回资源id,0标识未找到该资源     */    public int getIdentifier(String name, String defType, String defPackage) {        if (name == null) {            throw new NullPointerException("name is null");        }        try {            return Integer.parseInt(name);        } catch (Exception e) {            // Ignore        }        return mAssets.getResourceIdentifier(name, defType, defPackage);    }}

也就是说在任意的apk文件中,只需要知道包名(manifest.xml中指定的包名,用于寻找资源和Java类)、资源类型名称、资源描述名称。
比如:在包A中有一个defType"color"namecolor_red_1的属性,通过Resource#getIdentifier则可以获取包B中该名称的颜色资源。

//将skina重View的背景色设置为com.example.skinb中所对应的颜色if (attrValue.startsWith("@") && attrName.contains("background")){int resId = Integer.parseInt(attrValue.substring(1));int originColor = mContext.getResources().getColor(resId);        if (mResource == null){            return originColor;        }        String resName = mContext.getResources().getResourceEntryName(resId);        int skinRealResId = mResource.getIdentifier(resName, "color", "com.example.skinb");        int skinColor = 0;        try{            skinColor = mResource.getColor(skinRealResId);        }catch (Exception e){            Log.e(TAG, "", e);            skinColor = originColor;        }        view.setBackgroundColor(skinColor);}

上述方法也是换肤框架Android-Skin-Loader的基本思路。

参考文章

  1. 遇见LayoutInflater&Factory
  2. Android 探究 LayoutInflater setFactory
  3. Android换肤原理和Android-Skin-Loader框架解析
  4. Android中插件开发篇之----应用换肤原理解析

更多相关文章

  1. [Android5.1]开机动画显示工作流程分析
  2. Android(安卓)Fragment 真正的完全解析(下)
  3. 删除Android工程中冗余资源
  4. Android(安卓)缓存框架 ASimpleCache
  5. Android之通知的使用-Notification
  6. Android客户端post请求服务器端实例
  7. Android开发经验总结——ListView的使用
  8. Android关于ViewPager+Fragment缓存问题
  9. Android(安卓)开发中Parcel存储类型和数据容器

随机推荐

  1. Android当方法总数超过64K时(Android(安
  2. Android(安卓)Media Scanner Process
  3. android 导入工程出现很多错误 cannot be
  4. android之无返回结果跳转intent
  5. android折叠展开列表测试
  6. android webview 面试
  7. android中调用金山词霸
  8. android帧动画
  9. android获得手机的电量
  10. android 使用shape来优化界面效果