Hook换肤原理
安卓中的资源引用
常见的资源引用方式
-
@packageName:type/name
引用自己的资源:
android:text="@string/hello"
-
引用系统公共资源
引用系统资源:
android:textColor="@android:color/opaque_red"
-
引用系统非公共资源
引用安卓非公共资源
android:testSize= @*android:type/name
-
? 引用主题属性
?[namespace:]type/name
们使用一个"?"前缀代替了"@"。当你使用这个标记时,你就提供了属性资源的名称,它将会在主题中被查找,所以你不需要显示声明这个类型(如果声明,其形式就是?android:attr/android:textDisabledColor)。除了使用这个资源的标识符来查询主题中的值代替原始的资源,其命名语法和"@"形式一致:?[namespace:]type/name,这里类型可选android:textColor="?android:textDisabledColor"
android:textColor="?android:attr/android:textDisabledColor"
-
@+表示创建或者引用一个为定义的资源
- ”+”表示在R.java中名为type的内部类中添加一条记录。如"@+id/button"的含义是在R.java 文件中的id 这个静态内部类添加一条常量名为button
- @id/资源ID名 应用现有已定义的资源ID,包括系统ID
- @android:id/资源ID名 引用系统ID,其等效于@id/资源ID名
<TextView android:focusable="?attr/changeable" // 第四种情况,引用自己主题中的值 android:textColorHint="@*android:color/holo_green_dark"// 使用系统非public资源 android:textColor="@net.sunniwell.course.hook:color/colorBlack"// 使用自己定义的color, 如果是自己的包中资源,可以省略包名 虽然这里可以指定包名,但是如果你用其他应用的包名,是不行的, android:text="@string/app_name"// 引用自己包中的资源省略包名 android:background="@android:color/holo_green_dark" // 使用系统公共资源 android:id="@+id/main_tv" // 创建一个新的资源id android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
- 系统资源定义分public和非public。public的声明在:
<sdk_path>\platforms\android-28\data\res\values\public.xml
- @*android:type/name:可以调用系统定义的所有资源
- @android:type/name:只能够调用publi属性的资源。
针对第四种情况,如果想引用自己设置的 需要遵循以下步骤:
-
在attrs 中声明自己的命名空间
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="styleAble"> <attr name="changeable" format="boolean"/> </declare-styleable> </resources>
-
在theme中对这个值进行声明
<!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <item name="changeable">true</item> </style>
-
在控件中使用这个值
<TextView android:focusable="?attr/changeable" />
尽量不要使用系统私有资源,这样是危险的
Theme Style attr 区别
- Thream 是指一系列的属性集合, 既可以包含Style也可以直接包含一个attr标签属性。 Theme指定的作用方向是一个应用或者一个Activity。 Theme是整个应用(或者Activity)所引用到的资源的集合。 通过Theme你可以设置 一些比较重要通用的属性。
比如设置windowbackground, 设置listview的divider等
Style主要是针对某一个控件的属性值, 例如设置一个控件的宽,高,背景等,style属性中的属性,都需要被控件可以识别。
Theme可以使用者以及其子控件
App的theme中包含各种控件的默认style,这样即使没有设置控件的style,那么也可以使用在app theme中
控件中指定style,那么执行的sytle就会覆盖Theme中默认的style。
Theme加载流程
- Activity#setContentView
- PhoneWindow#installDecor
- PhoneWindow#generateLayout
- Window#getWindowStyle
- Context#obtainStyledAttributes
- ContextThemeWapprer#getTheme
- ContextThemeWapprer#setTheme
- Resources#selectDefaultTheme
- Resources#selectSystemTheme
- Resources#onApplyThemeResource
----------------------------------------------------------------LEVEL1 --------------------------------------------------------------------------
Theme.AppCompat, Theme.AppCompat.Light, Theme.AppCompat.NoActionBar 等主题在这里被定义。开发者应该使用这些主题,而非那些更底层的。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar"/> // 在 com.android.support:appcompat-v7:28.0.0
----------------------------------------------------------------LEVEL2 --------------------------------------------------------------------------
根据安卓的官方解释,我们在这一层拿到的主题只是第三层主题的别名:<style name="Base.Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light"> // 这个parent主题针对安卓进行了版本适配,在不同的版本中有不同的配置,这个主要是为了同安卓的原生主题进行适配。保证版本上的app主题同安卓主题一致。 <style name="Base.Theme.AppCompat.Light" parent="Base.V21.Theme.AppCompat.Light"/>
----------------------------------------------------------------LEVEL3 --------------------------------------------------------------------------
大部分工作在这一层被完成,Base.V7.Theme.AppCompat, Base.V11.Theme.AppCompat, Base.V21.Theme.AppCompat 等也是在这一层被定义。这些主题都继承自 Platform.AppCompat。
绝大多数属性和几乎所有工作在 Base.V{api}.Theme.AppCompat 中被定义和完成。ActionBar, DropwDown, ActionMode, Panel, List, Spinner, Toolbar 等控件中的所有属性都在这里被定义。
<style name="Base.V21.Theme.AppCompat.Light" parent="Base.V7.Theme.AppCompat.Light">
<style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat">
----------------------------------------------------------------LEVEL4 --------------------------------------------------------------------------
<style name="Platform.AppCompat" parent="android:Theme.Holo"> Platformstyle也针对版本进行了适配。
<style name="Theme.Holo">// 当前版本的默认主题
Hook换肤
什么是换肤
有时候,我们的app主题是一个统一的颜色,但是为了配合其他一些场景(节日,促销等活动),会在特定的时间段再不改变app基本控件功能、位置的情况下,修改app的控件背景颜色,图片src,字体颜色,从而给用户一个新的体验。
为什么换肤
- 保持app的新鲜感
- 提高付费率
- 配合节日,活动进行营销
- apk瘦身
- 写死的主题,切换很low
如何换肤
具体效果或者具体要求
- 不闪烁
- 不用启动
- 架构独立
- 无继承
- 易维护,可以多次复用
哪些东西可以替换
放在res目录下的东西基本都可以进行替换。 那么控件的大小可以进行设置吗? 等下我们试试。
- 背景颜色
- 文字颜色
- 图片
- 字体
- 动画
- 音频
Hook换肤的基本方案
- 通过定义多个theme, theme中使用不同的颜色或者图片,从而达到不同的效果。缺点: theme需要集成在app中,并且如果想要重新设置theme,需要重新启动app
- 统计app内需要设置换肤的控件,然后针对这些控件设置背景或者背景颜色。
View加载流程--->换肤
我们在Activity.setContentView之后,系统是如何把xml文件转换成一个个的View的呢?来吧, RTFS。
@SystemService(Context.LAYOUT_INFLATER_SERVICE)
public abstract class LayoutInflater {
// these are optional, set by the caller
private boolean mFactorySet; // 表示当前factory是否已经被设置了,只能设置一次
private Factory mFactory;
private Factory2 mFactory2;
private Factory2 mPrivateFactory;
// 通常我们直接通过这个方法获取到的inflater是系统直接创建好的。
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
// 这个方法中,setfactory 是设置mFactory
public void setFactory(Factory factory) {
if (mFactorySet) { // 只能设置一次,如果设置两次可能包错
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = factory;
} else {// 如果当前的mFactory不为空,那么就进行合并 ,不要使用这个,因为这个可能出现空指针
mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
}
}
// 同样setFactory2
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory; // 如果mFactory为空,那么mFactory和mFactory2都用factory
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
// setPrivateFactory,这个方法是hide的,提供给framework使用
public void setPrivateFactory(Factory2 factory) {
if (mPrivateFactory == null) {
mPrivateFactory = factory;
} else {
mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
}
}
// 重要的inflate方法,将resource 填充到viewParent里面
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
// 通过resId,生成一个xml解析器,使用xml pull解析
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
// 最终调用到的方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// 保存rootview
View result = root;
int type;
// 首先将parse跳转到第一个开始的地方
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
// 如果是merge标签,
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {// 如果是merge标签,但是又没有根view,报错
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 通过createViewFromTag来生成一个tempView
//******************************重要方法*********************************//
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 通过addView,将 当前生成的View加入到Viewroot里面。
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 如果是添加到root里面,返回rootView, 否则返回tempView
if (root == null || !attachToRoot) {
result = temp;
}
return result;
}
}
//对于一个View标签,通过反射创建一个View,并把参数设置进去
// parent viewRoot
//name 当前View 的标签
// context 生成View 的context
// 当前View下的属性标签集合
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// 如果是一个blink标签, 那么就创建一个BliknLayout
// 这个Blink其实就一个闪烁的标签 ,不要尝试使用它, 因为他一闪一闪的让人很难受,
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
if (mFactory2 != null) {
// 首先使用mFractory2来创建这个View
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
// 如果mFactory不为空,那么就是用mFactory来创建,不过一般情况下mFactory都是为空
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
// 如果上面都没有创建成功,就是用mPrivateFactory来创建
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
// 如果直接是 Button, TextView这样的标签, 在这里创建了一些安卓基础控件
view = onCreateView(parent, name, attrs);
} else {
// 如果是 android.support.constraint.ConstraintLayout 这样的标签,
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;
}
}
// 创建安卓基础控件
// prefix android.view.
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//首先获取这个View 的构造函数, 如果之前缓存过, 那么直接用,如果没有缓存过,那么就创建构造函数,并缓存
// 这个也是安卓里面常用的缓存策略。
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {// 这里还针对构造函数进行了验证,
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 缓存池里面没有找到View的构造函数,所以需要先loadclass,并找到Constructor,并进行缓存
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
// 这里主要是过滤 是否可以inflate这个类,如果不能,那么就抛出异常
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
// 通过constructor.newInstance创建一个View实例。并且将View的attrs设置进去,保证View可以正常创建。
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
attrs.getPositionDescription() + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
}
APP资源加载引用流程
其实 app的资源是归 Resource来进行管理的。
Resource 里面包含了一个ResourceImpl, 而 ResourceImpl又包含了一个AssetManager。
AssetManager才是真正管理app资源的。
换肤方案
如果我们想实现好的换肤效果,那么我们需要解决以下几个问题
-
确定给哪些控件进行换肤
我们可以给控件增加一些自定义属性,通过设置改变这个自定义属性的值,来确定是否可以换肤,以及给这个控件设置什么样的皮肤资源。
-
拿到需要换肤的控件
拿到LayoutInflater, 给LayoutInflater设置一个Factory,通过Factory.onCreateView方法拦截View的生成,同时拿到需要换肤的View对象
-
拿到换肤的资源
使用反射创建一个AssetManager,设置皮肤资源的地址,然后给VIew设置皮肤资源
给需要换肤的控件设置换肤资源
按照这个基本思路,加上我们上面的分析,我们开始进行解决。
针对AppCompateActivity
public class SWChangeSkinActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在调用super.onCreate之后,已经将AppCompatDelegateImpl对象设置成为Inflater的factory2,
// 在这一句里面,调用了getDelegate().installFactory
// 先看getDelegate() 实际获取到的是 AppCompatDelegateImpl, 最后调用的AppCompateDelegateImpl.installFactory
/*
**************************AppCompatActivity***************************
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
**************************AppCompatDelegate***************************
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}
**************************AppCompatDelegateImpl***************************
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
*/
setContentView(R.layout.activity_main);
//第二句 setcontentView 这个方法,走的是 AppCompatDelegateImpl中的 setcontentView
/*
**************************AppCompatDelegateImpl***************************
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent); // 这里就是将xml资源转换成View的关键代码
mOriginalWindowCallback.onContentChanged();
}
*/
}
}
所以,从上面的代码来看,我们针对appcompatDelegateActivity 在设置layoutfactory的时候,需要在super.onCreate之前进行调用。
代码详解
-
首先需要我们自定义属性,这样方便我们进行之后的收集,同时也可以指定更改什么资源
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- 创建自定义属性,表示当前ViewView是否可以进行theme更改--> <declare-styleable name="Skinable"> <attr name="themeChange" format="string"/>// 可以修改哪些主题 <attr name="isSupport" format="boolean"> // 是否支持进行主题修改 </attr> </declare-styleable> </resources> ------------------------------------------------------------------------------------------ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" app:themeChange = "background" android:orientation="vertical" app:isSupport="true" // 定义当前VIew支持修改主题 tools:context=".MainActivity"> <TextView android:background="@color/colorPrimary" app:isSupport="true" app:themeChange = "background.textcolor"// 定义当前view可以i修改 背景颜色和字体颜色。 android:layout_width="120dp" android:layout_height="40dp" android:gravity="center" android:text="Hello" android:textColor="@color/text_white" /> <ImageView app:themeChange = "src" app:isSupport="true" android:layout_width="200dp" android:layout_height="200dp" android:background="@color/white" android:src="@drawable/beauty1" /> <Button app:themeChange = "background" app:isSupport="true" android:background="@color/colorPrimary" android:id="@+id/main_btn_change_them" android:layout_width="120dp" android:layout_height="50dp" android:text="ChangeTheme" /> </LinearLayout>
-
创建自定义的LayoutInflater.Factory
package net.sunniwell.course.hooktheme.hook; import android.content.Context; import android.content.res.TypedArray; import android.os.SystemClock; import android.support.v7.app.AppCompatDelegate; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import android.view.ViewStub; import android.widget.ImageView; import android.widget.TextView; import net.sunniwell.course.hooktheme.R; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.HashMap; import java.util.jar.Attributes; /** * 通过自定义LayoutInflater来进行View创建。 */ public class SWLayoutFactory implements LayoutInflater.Factory2 { private static final String TAG = "SWLayoutFactory"; private AppCompatDelegate mDelegate; public SWLayoutFactory(AppCompatDelegate delegate) { mDelegate = delegate; } private ArrayList<ThemedView> viewToChangeBackground = new ArrayList<>(); private ArrayList<ThemedView> viewToChangeImageSource = new ArrayList<>(); private ArrayList<ThemedView> viewToChangeTextColor = new ArrayList<>(); /** * 参考PhoneLayoutInflater */ private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app." }; /** * Empty stack trace used to avoid log spam in re-throw exceptions. */ private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; final Object[] mConstructorArgs = new Object[2]; static final Class<?>[] mConstructorSignature = new Class[]{ Context.class, AttributeSet.class}; private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>(); /** * 从LayoutInflater.createViewFromTag 这个函数中,我们可以看到,只会调用这个方法, 第二个方法可以不管 * * @param parent * @param name * @param context * @param attrs * @return */ @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { View v = mDelegate.createView(parent, name, context, attrs); // 首先指定使用父类的进行创建,这样避免出现appcompat类中无法进行适配的问题。 if (v != null) { collectThemeChangeableView(context, v, attrs); return v; } // 如果父类无法正常创建,我们来接管创建流程 if (name.indexOf(".") == -1) {// 如果是 Button, text这样的view,需要添加前缀 for (String prefix : sClassPrefixList) { try { View view = createView(context, name, prefix, attrs); if (view != null) { return view; } } catch (Exception e) { // In this case we want to let the base class take a crack // at it. } } } else { // 其他的都是不用添加前缀, try { View view = createView(context, name, null, attrs); if (view != null) { return view; } } catch (Exception e) { // In this case we want to let the base class take a crack // at it. } } // 如果我们自己没办法创建,交给系统创建 return null; } /** * 这个方法我们可以不进行处理,没有调用到这个方法。 * * @param name * @param context * @param attrs * @return */ @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } // 我们自定义的view创建类,具体流程可以参见PhoneLayoutInflater.java,LayoutInflater.java public final View createView(Context ctx, String name, String prefix, AttributeSet attrs) { Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; try { if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = ctx.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } Object lastContext = mConstructorArgs[0]; if (mConstructorArgs[0] == null) { // Fill in the context if not already within inflation. mConstructorArgs[0] = ctx; } Object[] args = mConstructorArgs; args[1] = attrs; final View view = constructor.newInstance(args); mConstructorArgs[0] = lastContext; // TODO 在这里针对View进行统计 collectThemeChangeableView(ctx, view, attrs); return view; } catch (NoSuchMethodException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e); ie.setStackTrace(EMPTY_STACK_TRACE); Log.d(TAG, "createView: ie:" + ie.toString()); } catch (ClassCastException e) { // If loaded class is not a View subclass final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e); ie.setStackTrace(EMPTY_STACK_TRACE); Log.d(TAG, "createView: ie:" + ie.toString()); } catch (ClassNotFoundException e) { // If loadClass fails, we should propagate the exception. Log.d(TAG, "createView: ie:" + e.toString()); } catch (Exception e) { final InflateException ie = new InflateException( attrs.getPositionDescription() + ": Error inflating class " + (clazz == null ? "<unknown>" : clazz.getName()), e); ie.setStackTrace(EMPTY_STACK_TRACE); Log.d(TAG, "createView: ie:" + ie.toString()); } finally { } return null; } /** * 到这里,我们已经基本确定了哪些View需要执行一键换肤,以及这些view需要更改什么资源,到这里第二大步骤已经完成了 * 接下来我们需要进行第三步的处理, 加载外部的主题资源 ,加载外部的主体资源主要是通过Resource这个类完成的。 * * @param ctx * @param v * @param attrs */ private void collectThemeChangeableView(Context ctx, View v, AttributeSet attrs) { TypedArray typedArray = ctx.obtainStyledAttributes(attrs, R.styleable.Skinable); boolean b = typedArray.getBoolean(R.styleable.Skinable_isSupport, false); // 如果这个view包含我们自定义的支持换肤的属性,那么就需要进行解析。 if (b) { String s = typedArray.getString(R.styleable.Skinable_themeChange); Log.d(TAG, "collectThemeChangeableView: start to collect ---------------------------"); if (!TextUtils.isEmpty(s)) { int attributeCount = attrs.getAttributeCount(); HashMap<String, String> attrMap = new HashMap<>(); for (int i = 0; i < attributeCount; i++) {//遍历所有属性 String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); Log.d(TAG, "collectThemeChangeableView: attrName;" + attrName + ";attrValue:" + attrValue); attrMap.put(attrName, attrValue);//全部存起来 } boolean containPoint = s.contains("."); int count = 1; String[] split = new String[]{s}; if (containPoint) { split = s.split("\\."); count = split.length; } // 根据定义中的 background ,textcolor, src 等来确定需要更改哪些主题资源 for (int i = 0; i < count; i++) { if (split[i].equals("background")) { String attrValue = attrMap.get("background"); viewToChangeBackground.add(new ThemedView(v, attrValue)); } else if (split[i].equals("src")) { String attrValue = attrMap.get("src"); viewToChangeImageSource.add(new ThemedView(v, attrValue)); } else if (split[i].equals("textcolor")) { String attrValue = attrMap.get("textColor"); viewToChangeTextColor.add(new ThemedView(v, attrValue)); } /*....下面可以针对这些进行更多的适配。.....*/ } } } Log.d(TAG, "collectThemeChangeableView: end to collect --------------------------- \n"); typedArray.recycle(); } public void changeTheme() { // 我们现在只统计了三个部分, background textcolor src Log.d(TAG, "changeTheme: " + SystemClock.currentThreadTimeMillis() + "\n"); for (int i = 0; i < viewToChangeTextColor.size(); i++) { ThemedView v = viewToChangeTextColor.get(i); int bgId = Integer.parseInt(v.resourceId.substring(1));//属性值,R.id.XXX ,int类型, ((TextView) v.mView).setTextColor(ThemeResourceEngine.getInstance().getColor(bgId)); Log.d(TAG, "changeTheme: changeTextColor: " + i + "; resId:" + v.resourceId); } for (int i = 0; i < viewToChangeImageSource.size(); i++) { ThemedView v = viewToChangeImageSource.get(i); int bgId = Integer.parseInt(v.resourceId.substring(1));//属性值,R.id.XXX ,int类型, Log.d(TAG, "changeTheme: setImageResource: " + i + "; resId:" + v.resourceId); ImageView iv = ((ImageView) v.mView); iv.setImageDrawable(ThemeResourceEngine.getInstance().getDrawable(bgId)); } for (int i = 0; i < viewToChangeBackground.size(); i++) { ThemedView v = viewToChangeBackground.get(i); int bgId = Integer.parseInt(v.resourceId.substring(1));//属性值,R.id.XXX ,int类型, Log.d(TAG, "changeTheme: setBackgroundColor: " + i + "; resId:" + v.resourceId); String attrType = v.mView.getResources().getResourceTypeName(bgId); // 属性类别:比如 drawable ,color Log.d(TAG, "changeTheme: attrType:" + attrType); if (TextUtils.equals(attrType, "drawable")) {//区分drawable和color v.mView.setBackgroundDrawable(ThemeResourceEngine.getInstance().getDrawable(bgId));//加载外部资源管理器,拿到外部资源的drawable } else if (TextUtils.equals(attrType, "color")) { v.mView.setBackgroundColor(ThemeResourceEngine.getInstance().getColor(bgId)); } } Log.d(TAG, "changeTheme: " + SystemClock.currentThreadTimeMillis()); } /** * 因为我们后面需要根据原来的资源id来获取到新的资源id,所以,我们必须要保存和这个view同步的attrs */ private class ThemedView { // 保留原来的属性值,这样可以非常方便的进行主题修改。 public View mView; public String resourceId; public ThemedView(View v, String a) { mView = v; resourceId = a; } } }
-
创建外部资源加载类
加载外部的主题资源。
package net.sunniwell.course.hooktheme.hook; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.Log; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * 主要用来加载外部的主题资源。 */ public class ThemeResourceEngine { private static final String TAG = "ThemeResourceEngine"; private static ThemeResourceEngine mInstance; private ThemeResourceEngine() { } public static ThemeResourceEngine getInstance() { if (mInstance == null) { synchronized (ThemeResourceEngine.class) { if (mInstance == null) { mInstance = new ThemeResourceEngine(); } } } return mInstance; } private Resources mOutResource; private String outPackageName; private Context mCtx; public void loadResource(String path, Context ctx) { if(TextUtils.isEmpty(path)){ Log.d(TAG, "loadResource: path is emgpt"); } File cacheFile =ctx.getCacheDir(); File apkFile = new File(cacheFile,"app-debug.apk"); if(apkFile.exists()){ Log.d(TAG, "loadResource: current apkFile exist:"+ apkFile.getAbsolutePath()); }else{ Log.d(TAG, "loadResource: apkfile not exist"); return; } path = apkFile.getAbsolutePath(); mCtx = ctx; AssetManager manager = null; try { PackageManager packageManager = ctx.getPackageManager(); PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES); outPackageName = packageArchiveInfo.packageName; Log.d(TAG, "loadResource: getPackagename: " +outPackageName); Constructor<AssetManager> constructor = AssetManager.class.getConstructor(null); constructor.setAccessible(true); manager = constructor.newInstance(null); Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);//为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用 addAssetPath.invoke(manager, path);//反射执行方法 mOutResource = new Resources(manager, ctx.getResources().getDisplayMetrics(), ctx.getResources().getConfiguration()); Log.d(TAG, "loadResource: outResource: " + mOutResource); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } public int getColor(int res) { if (mOutResource == null) { return res; } Log.d(TAG, "getColor: outColor is not null:"); String name = mOutResource.getResourceEntryName(res); int outResId = mOutResource.getIdentifier(name, "color", outPackageName); if (outResId == 0) { return res; } return mOutResource.getColor(outResId); } /** * 提供外部资源包里的图片资源 * * @param resId * @return */ public Drawable getDrawable(int resId) {//获取图片 if (mOutResource == null) { return ContextCompat.getDrawable(mCtx, resId); } String resName = mOutResource.getResourceEntryName(resId); int outResId = mOutResource.getIdentifier(resName, "drawable", outPackageName); if (outResId == 0) { return ContextCompat.getDrawable(mCtx, resId); } return mOutResource.getDrawable(outResId); } }
-
使用:
public class SWApplication extends Application { @Override public void onCreate() { super.onCreate(); ThemeResourceEngine.getInstance().loadResource("", this); } } -------------------------------------------------- public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private SWLayoutFactory mLayoutFactory; @Override protected void onCreate(Bundle savedInstanceState) { changeLaoutInflaterFactory2(LayoutInflater.from(MainActivity.this)); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.main_btn_change_them).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d(TAG, "onClick: onButtonClick"); mLayoutFactory.changeTheme(); } }); } private void changeDelegate() { final AppCompatDelegate delegate = getDelegate(); Log.d(TAG, "changeDelegate: Class:" + delegate.getClass()); Object proxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{LayoutInflater.Factory2.class, MenuBuilder.Callback.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.d(TAG, "methodName:" + method.getName()); return method.invoke(delegate, args); } }); // private AppCompatDelegate mDelegate; try { Field f = AppCompatActivity.class.getDeclaredField("mDelegate"); f.setAccessible(true); Object originalDelegate = f.get(this); f.set(this, proxy); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } /** * 第一大步 首先将LayoutInflater的factory2换成我们自己定义的LayoutInflater,通过自定义LayoutInflater来截获 * View的生成过程,在这个过程中统计目前支持换肤的元素 * * @param inflater */ private void changeLaoutInflaterFactory2(LayoutInflater inflater) { if (inflater == null) { Log.d(TAG, "sorry, current layoutinflater is null"); return; } if (mLayoutFactory == null) { mLayoutFactory = new SWLayoutFactory(getDelegate()); } Log.d(TAG, "changeLaoutInflaterFactory2: inflaterCalss: " + inflater.getClass()); Field f = Util.getField(inflater.getClass(), "mFactorySet"); boolean o = true; try { o = (boolean) f.get(inflater); Log.d(TAG, "changeLaoutInflaterFactory2: mFactorySet:" + o); } catch (IllegalAccessException e) { e.printStackTrace(); } LayoutInflater.Factory2 factory2 = inflater.getFactory2(); Log.d(TAG, "changeLaoutInflaterFactory2: factory2:" + factory2); if (!o) { inflater.setFactory2(mLayoutFactory); } else { if (Util.setField(inflater, "mFactorySet", false)) { inflater.setFactory2(mLayoutFactory); } } } }