Android 源码系列之<五>从源码的角度深入理解LayoutInflater.Factory之主题切换(中)

转载请注明出处: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是没有效果的,另外代码的通用性也不强,所以在下篇文章中要处理这些问题,敬请期待...

时间: 2024-07-30 23:55:11

Android 源码系列之<五>从源码的角度深入理解LayoutInflater.Factory之主题切换(中)的相关文章

Android 源码系列之&lt;四&gt;从源码的角度深入理解LayoutInflater.Factory之主题切换(上)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51252401 现在越来越多的APP都加入了主题切换功能或者是日间模式和夜间模式功能切换等,这些功能不仅增加了用户体验也增强了用户好感,众所周知QQ和网易新闻的APP做的用户体验都非常好,它们也都有日间模式和夜间模式的主题切换功能.体验过它们的主题切换后你会发现大部分效果是更换相关背景图片.背景颜色.字体颜色等来完成的,网上这篇文章对主题切换讲解的比较不错,今天我们从源码的角度来学习一下

Android 源码系列之&lt;十一&gt;从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(下)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52843637 在上篇文章Android 源码系列之<十>从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)中我们讲解了通过AccessibilityService实现自动安装APK小外挂的操作流程,如果你还没有看过上篇文章请点击这里.在这篇文章中我将带领小伙伴从源码的角度来深入学习一下AccessibilityServie的技术实现原理,希望这

hbase源码系列(五)单词查找树

在上一章中提到了编码压缩,讲了一个简单的DataBlockEncoding.PREFIX算法,它用的是前序编码压缩的算法,它搜索到时候,是全扫描的方式搜索的,如此一来,搜索效率实在是不敢恭维,所以在hbase当中单独拿了一个工程出来实现了Trie的数据结果,既达到了压缩编码的效果,亦达到了方便查询的效果,一举两得,设置的方法是在上一章的末尾提了. 下面讲一下这个Trie树的原理吧. hbase源码系列(五)单词查找树,布布扣,bubuko.com

Android 源码系列之&lt;十三&gt;从源码的角度深入理解LeakCanary的内存泄露检测机制(中)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52958563 在上篇文章Android 源码系列之<十二>从源码的角度深入理解LeakCanary的内存泄露检测机制(上)中主要介绍了Java内存分配相关的知识以及在Android开发中可能遇见的各种内存泄露情况并给出了相对应的解决方案,如果你还没有看过上篇文章,建议点击这里阅读一下,这篇文章我将要向大家介绍如何在我们的应用中使用square开源的LeakCanary库来检测应用中出

Spark源码系列(五)RDD是如何被分布式缓存?

这一章想讲一下Spark的缓存是如何实现的.这个persist方法是在RDD里面的,所以我们直接打开RDD这个类. def persist(newLevel: StorageLevel): this.type = { // StorageLevel不能随意更改 if (storageLevel != StorageLevel.NONE && newLevel != storageLevel) { throw new UnsupportedOperationException("C

Android 源码系列之&lt;十&gt;从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52822148 说起外挂特别是玩游戏的小伙伴估计对它很熟悉,肯定有部分小伙伴使用过,至于为什么使用它,你懂得(*^__^*) --我最早接触外挂是在大二的时候,那时候盛行玩QQ农场,早上一睁眼就是打开电脑先把自己的菜收了,收完之后再去偷别人的:后来童靴说非凡软件上有一个偷菜外挂,于是赶紧整了一个,有了外挂之后就告别了体力时代,省时又省力--既然在PC上有外挂,那在智能手机上可以做外挂呢?

我的Android 4 学习系列之开始入手:配置开发环境与理解Hello World!

p { padding-left: 10px; } 目录 如何安装Android SDK.创建开发环境和调试项目 移动设计中一些注意事项 使用Android虚拟设备.模拟器和其他开发工具 如何安装Android SDK.创建开发环境和调试项目 下载和安装Android SDK : 我的是window7系统,当然下载 SDK starter package 最合适了: http://developer.android.com/sdk/index.html 下载完打开压缩包如下: 然后把这个包解压到

Android 源码系列之&lt;七&gt;从源码的角度深入理解IntentService及HandlerThread

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51373243 提起Service大家都很熟悉,它乃Android四(si)大(da)组(jing)件(gang)之一.但是说起IntentService有童靴或许有点陌生,看名字感觉和Service有关连.不错,不仅有关联而且关系还不一般,IntentService是Service的子类,所以它也是正宗的Service,由于IntentService借助了HandlerThread,我

Android 源码系列之&lt;一&gt;从源码的角度深入理解ImageView的ScaleType属性

做Android开发的童靴们肯定对系统自带的控件使用的都非常熟悉,比如Button.TextView.ImageView等.如果你问我具体使用,我会给说:拿ImageView来说吧,首先创建一个新的项目,在项目布局文件中应用ImageView控件,代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.