Android主题更换换肤
Android主题更换换肤
文章目录
知识总览
认识setFactory
获取任意一个apk压缩文件的Resource对象
1、如何创建自定义的Resource实例
2、如何知道当前属性值在所在Resource中的id
参考文章
知识总览
android主题换肤通常借助LayoutInflater#setFactory实现换肤。
换肤步骤:
通过解析外部的apk压缩文件,创建自定义的Resource对象去访问apk压缩文件的资源。
借助LayoutInfater#setFactoy,将步骤(1)中的资源应用到View的创建过程当中。
认识setFactory
平常设置或者获取一个View时,用的较多的是setContentView或LayoutInflater#inflate,setContentView内部也是通过调用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#onCreateView和Factory#onCreateView进行创建,然后走默认的创建流程。所以,我们可以在此处创建自定义的Factory2或Factory,并将自定义的Factory2或Factory对象添加到LayoutInflater对象当中,来对View的创建进行干预,LayoutInflate也提供了相关的API供我们添加自己的ViewFactory。
例如:下面我们通过设置LayoutInflater的Factory来,将视图中的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= @2131230758
name = Button, attrName = background, attrValue= @2131034152
name = Button, attrName = layout_width, attrValue= -2
name = Button, attrName = layout_height, attrValue= -2
name = Button, attrName = id, attrValue= @2131230757
name = Button, attrName = background, attrValue= @2131034150
name = Button, attrName = layout_width, attrValue= -2
name = 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",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); 
}
上述方法也是换肤框架Android-Skin-Loader的基本思路。
参考文章
遇见LayoutInflater&Factory
Android 探究 LayoutInflater setFactory
Android换肤原理和Android-Skin-Loader框架解析
Android中插件开发篇之----应用换肤原理解析
作者:d袋鼠b 
来源:CSDN 
原文:https://blog.csdn.net/weixin_36570478/article/details/91464020 
版权声明:本文为博主原创文章,转载请附上博文链接!
关注公众号
					低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 
							
								
								    上一篇
								    
								
								华为鸿蒙操作系统,究竟会在何时发布?
对于华为来说,当下它最值得关注的产品,除了 5G,当属它自研的操作系统鸿蒙了。不过,二者的不同之处在于:华为 5G 已经有广泛的产品发布,形成了一个端到端的解决方案,而且华为创始人任正非也多次表态华为 5G 是世界领先的;而鸿蒙直到今天依然没有公开露面。 所以问题来了,鸿蒙到底什么时候发布? 最快 10 月发布?官方回应来了 近日,有境外媒体发布了《华为鸿蒙系统 最快 10 月发布》的报道;报道称,尽管美国总统特朗普曾经在 G20 之后对外公开表态会对华为 “松绑” ,但是就是否会允许 Google 继续对华为授权 Android 操作系统的问题,还没有明确的说法。 由此该报道援引了华为高管的表态称,鸿蒙系统的发布时间最早是在今年 10 月,最晚是在明年四月;并且该报道还认为鸿蒙系统将首发在华为的下半年旗舰机型 Mate 30 系列或者明年上半年的 P40 系列上。 然而,对此报道,华为官方进行了否认。 华为表示,最近没有提到鸿蒙系统的具体发布时间。华为方面还表示: 第一,一个开放的 Android 平台仍然是首选,可能的话,将继续优先采用 Android 系统。第二,操作系统在技术上...
 - 
							
								
								    下一篇
								    
								
								在Android 上运行 openCV ,并做灰度变化的一个例子
在Android 上运行 openCV ,并做灰度变化的一个例子OpenCVImageProcessing 导入Opencv的 androrid SDK灰度算法 OpenCVImageProcessing 导入opencv Jar包,配置OpenCVLibrary340 的 bulid.gradle , 配置Module:app 的 build.gradle , 在依赖里添加 implementation fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar')1在Gradle Scripts 里修改 dependencies  dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar') implementation 'com.android.support:app...
 
相关文章
文章评论
共有0条评论来说两句吧...

			
				
				
				
				
				
				
				
微信收款码
支付宝收款码