标签:
转载请注明出处:http://blog.csdn.net/llew2011/article/details/51287391
在上篇文章中我们主要讲解了LayoutInflater渲染xml布局文件的流程,文中讲到如果在渲染之前为LayoutInflater设置了Factory,那么在渲染每一个View视图时都会调用Factory的onCreateView()方法,因此可以拿onCreateView()方法做切入口实现主题切换功能。如果你不清楚LayoutInflater的渲染流程,请点击这里。今天我们就从实战出发来实现自己的主题切换功能。
既然主题切换是依赖Factory的,那么就需要定义自己的Factory了,自定义Factory其实就是实现系统的Factory接口,代码如下:
public class SkinFactory implements Factory { @Override public View onCreateView(String name, Context context, AttributeSet attrs) { Log.e("SkinFactory", "==============start=============="); int attrCounts = attrs.getAttributeCount(); for(int i = 0; i < attrCounts; i++) { String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); Log.e("SkinFactory", "attrName = " + attrName + " attrValue = " + attrValue); } Log.e("SkinFactory", "==============end=============="); return null; } }自定义SkinFactory什么都没有做,仅仅在onCreateView()方法中循环打印了attrs包含的属性名和对应的属性值,然后返回了null。创建完SkinFactory之后就是如何使用它了,上篇文章中我们讲过在Activity中可以通过getLayoutInflater()方法获取LayoutInflater实例对象,获取到该对象之后就可以给该其赋值Factory了,代码如下:
public class MainActivity extends Activity { private LayoutInflater mInflater; private SkinFactory mFactory; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mFactory = new SkinFactory(); mInflater = getLayoutInflater(); mInflater.setFactory(mFactory); setContentView(R.layout.activity_skin); } }需要注意的是给Activity的LayoutInflater设置Factory时一定要在调用setContentView()方法之前,否则不起作用。设置好Factory之后,我们看看一下activity_skin.xml布局文件是如何定义的,代码如下:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/color_app_bg" > <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Factory的小练习" android:textColor="@color/color_title_bar_text" /> </FrameLayout>布局文件居中显示了一个TextView,并且给TextView设置文本为"Factory的小练习",运行一下程序,打印结果如下:
这里只贴出了TextView的打印数据,从打印出的数据可以发现如果属性值是以@开头就表示该属性值是一个应用(以后可以通过@符号来判断当前属性是否是引用)。因为我们可以在attrs中拿到View在布局文件中定义的所有属性,所以可以猜想:如果给View添加自定义属性,在onCreateView()方法中通过解析这个自定义属性就可以判别出要做主题切换的View了。这个猜想正不正确,我们来试验一下。
在values文件夹下创建attrs.xml属性文件,定义属性名为enable,属性值为boolean类型(true表示需要主题切换,false表示不需要主题切换),代码如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="enable" format="boolean" /> </resources>定义完属性后,若要使用该属性需要先申明命名空间,比如系统自带的:xmlns:android="http://sckemas.android.com/apk/res/android",申明命名空间有两种方法:xmlns:skin="http://schemas.android.com/apk/包名"或者是xmlns:skin="http://schemas.android.com/apk/res-auto"。我们采用第二种写法,代码如下:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:skin="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/color_app_bg" > <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Factory的小练习" skin:enable="true" android:textColor="@color/color_title_bar_text" /> </FrameLayout>在activity_skin.xml布局文件中给TextView添加了自定义的enable属性并把值设为true,添加完属性后编译器报错提示说TextView没有该属性,只要手动清理一下就好了。然后运行代码,打印结果如下:
看到打印结果我们心里好happy呀,采用给View添加自定义的属性这种方式是OK的,接下来我们就可以根据该属性区分出哪些View需要做主题切换了。做主题切换的前提是缓存那些需要做主题切换的View,但是View做主题切换可能需要更改背景,文字等。也就说一个View可能要更改多个属性,那这个属性就要求在不同的场景下对应不同的类型,所以可以抽象出代表属性的类BaseAttr,BaseAttr类有属性名,属性值,属性类型等成员变量,还要有一个抽象方法(该方法在不同的场景下有不同的实现,比如当前属性为background,那在BackgroundAttr实现中就应该是设置背景;若当前属性为textColor,那在TextColorAttr实现中就应该是设置文字颜色)。所以BaseAttr可以抽象如下:
public abstract class BaseAttr { public String attrName; public int attrValue; public String entryName; public String entryType; public abstract void apply(View view); }
定义好BaseAttr类之后就可以定义具体的实现类了,比如背景属性类BackgroundAttr,字体颜色改变类TextColorAttr等,BackgroundAttr代码如下:
public class BackgroundAttr extends BaseAttr { @Override public void apply(View view) { if(null != view) { view.setBackgroundXXX(); } } }
抽象出属性类BaseAttr之后我们还要考虑缓存View的问题,因为一个View可能要对应多个BaseAttr,所以我们还要封装一个类SkinView,该类表示一个View对应多个BaseAttr,它还要提供更新自己的方法,所以代码如下:
public class SkinView { public View view; public List<BaseAttr> viewAttrs; public void apply() { if(null != view && null != viewAttrs) { for(BaseAttr attr : viewAttrs) { attr.apply(view); } } } }抽象属性类BaseAttr和SkinView定义完了,接下来就可以在SkinFactory中做缓存逻辑了,代码如下:
public class SkinFactory implements Factory { private static final String DEFAULT_SCHEMA_NAME = "http://schemas.android.com/apk/res-auto"; private static final String DEFAULT_ATTR_NAME = "enable"; private List<SkinView> mSkinViews = new ArrayList<SkinView>(); @Override public View onCreateView(String name, Context context, AttributeSet attrs) { View view = null; final boolean skinEnable = attrs.getAttributeBooleanValue(DEFAULT_SCHEMA_NAME, DEFAULT_ATTR_NAME, false); if(skinEnable) { view = createView(name, context, attrs); if(null != view) { parseAttrs(name, context, attrs, view); } } return view; } public final View createView(String name, Context context, AttributeSet attrs) { View view = null; if(-1 == name.indexOf('.')) { if("View".equalsIgnoreCase(name)) { view = createView(name, context, attrs, "android.view."); } if(null == view) { view = createView(name, context, attrs, "android.widget."); } if(null == view) { view = createView(name, context, attrs, "android.webkit."); } } else { view = createView(name, context, attrs, null); } return view; } View createView(String name, Context context, AttributeSet attrs, String prefix) { View view = null; try { view = LayoutInflater.from(context).createView(name, prefix, attrs); } catch (Exception e) { } return view; } private void parseAttrs(String name, Context context, AttributeSet attrs, View view) { int attrCount = attrs.getAttributeCount(); final Resources temp = context.getResources(); List<BaseAttr> viewAttrs = new ArrayList<BaseAttr>(); for(int i = 0; i < attrCount; i++) { String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); if(isSupportedAttr(attrName)) { if(attrValue.startsWith("@")) { int id = Integer.parseInt(attrValue.substring(1)); String entryName = temp.getResourceEntryName(id); String entryType = temp.getResourceTypeName(id); BaseAttr viewAttr = createAttr(attrName, attrValue, id, entryName, entryType); if(null != viewAttr) { viewAttrs.add(viewAttr); } } } } if(viewAttrs.size() > 0) { SkinView skinView = new SkinView(); skinView.view = view; skinView.viewAttrs = viewAttrs; mSkinViews.add(skinView); } } // attrName:textColor attrValue:2130968576 entryName:common_bg_color entryType:color private BaseAttr createAttr(String attrName, String attrValue, int id, String entryName, String entryType) { BaseAttr viewAttr = null; if("background".equalsIgnoreCase(attrName)) { viewAttr = new BackgroundAttr(); } else if("textColor".equalsIgnoreCase(attrName)) { viewAttr = new TextColorAttr(); } if(null != viewAttr) { viewAttr.attrName = attrName; viewAttr.attrValue = id; viewAttr.entryName = entryName; viewAttr.entryType = entryType; } return viewAttr; } private boolean isSupportedAttr(String attrName) { if("background".equalsIgnoreCase(attrName)) { return true; } else if("textColor".equalsIgnoreCase(attrName)) { return true; } return false; } public void applaySkin() { if(null != mSkinViews) { for(SkinView skinView : mSkinViews) { if(null != skinView.view) { skinView.apply(); } } } } }
SkinFactory中定义了装载SkinView类型的mSkinViews缓存集合,当解析到符合条件的View时就会缓存到该集合中。在onCreateView()方法中调用AttributeSet的getAttributeBooleanValue()方法检测是否含有enable属性,如果有enable属性并且属性值为true时我们自己调用系统API来创建View,如果创建成功就解析该View,分别获取其attrName,attrValue,entryName,entryType值取完之后创建对应的BaseAttr,然后加入缓存集合mSkinViews中,否则返回null。
创建完SkinFactory之后还需要创建一个主题资源管理器SkinManager,主题切换就是通过该管理器来决定的。所以其主要有以下功能:实现读取额外主题资源功能,恢复默认主题功能,更新主题功能等。
先看一下如何读取额外主题资源问题。做主题切换需要准备多套主题,这些主题其实就是一些图片,颜色等。有了素材之后我们还要考虑如何提供给APP素材的形式,是直接提供一个Zip包文件还是说做成一个apk文件的形式提供给APP?如果提供Zip包接下来的处理是解压该Zip包得到里边的素材然后解析读取,理论上来说这种方式是可行的,但是操作起来有点复杂。所以我们采用apk的形式,若希望访问素材apk中的资源如同在APP中访问资源一样,我们得获取到素材apk的Resources实例,下面我直接提供一种通用的可以获取apk的Resources实例代码,代码如下:
public final Resources getResources(Context context, String apkPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); addAssetPath.setAccessible(true); addAssetPath.invoke(assetManager, apkPath); Resources r = context.getResources(); Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration()); return skinResources; } catch (Exception e) { } return null; }
这段代码可以有效的获取到apk中的Resources实例,然后通过该Resources实例访问资源就如同我们在APP中直接访问自己资源一般,如果你对Android的资源访问机制很熟悉的话,很清楚这段代码为什么要这么写。不清楚也没关系,先暂时这么用,我会在后续文章中从源码的角度分析一下Android的资源访问机制并解释这么写的原因。
好了,现在我们已经解决了访问素材资源的问题,那接下来就是编写我们的SkinManager类了,SkinManager类的功能是来加载素材资源文件的,在加载文件时可能有失败的情况,所以需要给APP回调来通知加载资源的结果,我们定义接口ILoadListener,代码如下:
public interface ILoadListener { void onStart(); void onSuccess(); void onFailure(); }ILoadListener接口有三个方法,分别表示资源开始加载的回调,加载成功后的回调和加载失败后的回调。我们接着完成我们SkinManager代码,如下所示:
public final class SkinManager { private static final Object mClock = new Object(); private static SkinManager mInstance; private Context mContext; private Resources mResources; private String mSkinPkgName; private SkinManager() { } public static SkinManager getInstance() { if(null == mInstance) { synchronized (mClock) { if(null == mInstance) { mInstance = new SkinManager(); } } } return mInstance; } public void init(Context context) { enableContext(context); mContext = context.getApplicationContext(); } public void loadSkin(String skinPath) { loadSkin(skinPath, null); } public void loadSkin(final String skinPath, final ILoadListener listener) { enableContext(mContext); if(TextUtils.isEmpty(skinPath)) { return; } new AsyncTask<String, Void, Resources>() { @Override protected void onPreExecute() { if(null != listener) { listener.onStart(); } } @Override protected Resources doInBackground(String... params) { if(null != params && params.length == 1) { String skinPath = params[0]; File file = new File(skinPath); if(null != file && file.exists()) { PackageManager packageManager = mContext.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, 1); if(null != packageInfo) { mSkinPkgName = packageInfo.packageName; } return getResources(mContext, skinPath); } } return null; } @Override protected void onPostExecute(Resources result) { if(null != result) { mResources = result; if(null != listener) { listener.onSuccess(); } } else { if(null != listener) { listener.onFailure(); } } } }.execute(skinPath); } public Resources getResources(Context context, String apkPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); addAssetPath.setAccessible(true); addAssetPath.invoke(assetManager, apkPath); Resources r = context.getResources(); Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration()); return skinResources; } catch (Exception e) { } return null; } public void restoreDefaultSkin() { if(null != mResources) { mResources = null; mSkinPkgName = null; } } public int getColor(int id) { enableContext(mContext); Resources originResources = mContext.getResources(); int originColor = originResources.getColor(id); if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) { return originColor; } String entryName = mResources.getResourceEntryName(id); int resourceId = mResources.getIdentifier(entryName, "color", mSkinPkgName); try { return mResources.getColor(resourceId); } catch (Exception e) { } return originColor; } public Drawable getDrawable(int id) { enableContext(mContext); Resources originResources = mContext.getResources(); Drawable originDrawable = originResources.getDrawable(id); if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) { return originDrawable; } String entryName = mResources.getResourceEntryName(id); int resourceId = mResources.getIdentifier(entryName, "drawable", mSkinPkgName); try { return mResources.getDrawable(resourceId); } catch (Exception e) { } return originDrawable; } private void enableContext(Context context) { if(null == context) { throw new NullPointerException(); } } }
SkinManager我们采用了单例模式保证应用中只有一个实例,在使用的时候需要先进行初始化操作否则会抛异常。SkinManager不仅定义了属性mContext和mResources(mContext表示APP的运行上下文环境,mResources代表资源apk的Resources实例对象,如果为空表示使用默认APP主题资源),而且它还对外提供了一系列方法,比如读取资源的getColor()和getDrawable()方法,加载资源apk的方法loadSkin()等。
现在主题切换的核心逻辑都有了,我们看一下程序包结构图是怎样的,切图如下:
主题切换的核心逻差不多已经然完成了,接下来就是要练习使用一下看看效果能不能成了,首先修改activity_skin.xml布局文件,修改如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:skin="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/common_bg_color" android:orientation="vertical" skin:enable="true" > <FrameLayout android:layout_width="match_parent" android:layout_height="65dp" android:background="@color/common_title_bg_color" skin:enable="true" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="主题切换标题" android:textColor="@color/common_title_text_color" android:textSize="18sp" skin:enable="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right|center_vertical" android:onClick="updateSkin" android:text="切换主题" /> </FrameLayout> </LinearLayout>在activity_skin.xml布局文中给需要做主题切换的View节点添加了enable属性并且设置其值为true。接下来就是要做一个主题apk包了,做主题包的简单方式就是新建一个工程,里边不添加Activity等,然后在资源文件夹下创建对应的资源等,需要注意的是资源文件名一定要和APP中的资源名一致。然后编译打包成一个apk文件,这里就不再演示了。打包完apk后我们导入到模拟器根目录下,然后修改MainActivity,添加updateSkin()方法,代码如下:
public void updateSkin(View view) { String skinPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "skin.apk"; SkinManager.getInstance().loadSkin(skinPath, new ILoadListener() { @Override public void onSuccess() { mFactory.applaySkin(); } @Override public void onStart() { } @Override public void onFailure() { } }); }添加完updateSkin()方法之后,就可以实现切换主题了,为了方便我直接把skin.apk文件直接导入了SD卡根目录下,需要注意有的手机没有外置存储卡需要做个判断,别忘了在配置文件添加文件的读写权限,然后运行程序,效果如下:
好了,现在在当前页面进行主题切换看起来是OK的,但是还存在不足,当页面进行跳转比如从A→B→C→D然后在D中进行主题切换,这时候ABC是没有效果的,另外代码的通用性也不强,所以在下篇文章中要处理这些问题,敬请期待...
Android 源码系列之<五>从源码的角度深入理解LayoutInflater.Factory之主题切换(中)
标签:
原文地址:http://blog.csdn.net/llew2011/article/details/51287391