认识setFactory
平常用的较多的是LayoutInflater#inflate,而在.xml布局文件在解析的过程中会调用LayoutInflater#rInflate,随后会通过调用LayoutInflater#createViewFromTag来创建View。这里推荐《遇见LayoutInflater&Factory》下面一起看看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) { 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 { 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; } }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768从上述源码中可以看出View的创建过程中,会首先找Factory2#onCreateView和Factory#onCreateView进行创建,然后走默认的创建流程。所以,我们可以在此处创建自定义的Factory2或Factory来对View的创建进行干预。例如:可以将试图中的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); }123456789101112131415161718192021让后启动Activity后,会看到输出:name = Button, attrName = id, attrValue= @2131230758
name = 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= -212345678获取包外Resource我们通常通过Context#getSource()获取res目录下的资源,Context#getAssets()(想当于Context#getSource().getAssets())获取asset目录下的资源。所以要获取应用外的Resource资源文件,就需要使用应用外的文件信息创建Resource实例,然后通过这个实例获取文件中的资源信息。比如,新创建的的Resource实例为mResource,则可以使用mResource.getColor(colorId),来获取实例内colorId所对应的颜色。那么接下来的问题分为两步:如何创建自定义的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; } }1234567891011121314151617181920212223242526272829303132333435所以通过反射可以创建对应的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(http://www.my516.com); skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());} catch (Exception e) {
skinResource = null; } return skinResource; }123456789101112131415161718192021如何知道当前属性值在所在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); }}1
2345678910111213141516171819202122也就是说在任意的apk文件中,只需要知道包名(manifest.xml中指定的包名,用于寻找资源和Java类)、资源类型名称、资源描述名称。比如:在包A中有一个defType为"color",name为color_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);}---------------------