文章目录

    • 知识总览
    • 认识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 GridView实现选中图片放大。
  2. Android设置Settings实现:PreferenceActiv
  3. Android(安卓)Studio(八):Android(安卓)St
  4. Android(安卓)Bluetooth How To--Based o
  5. 活动的启动模式汇总
  6. Android(安卓)Studio 使用 Lambda表达式
  7. Android(安卓)Studio(四):Android(安卓)St
  8. 怎么在button上写两行文字
  9. [置顶] Android常用秘籍总结
  10. android事件分发机制总结