1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > Android实现动态换肤-原理篇

Android实现动态换肤-原理篇

时间:2021-10-01 06:08:00

相关推荐

Android实现动态换肤-原理篇

学习是一个过程。

文章目录

Activity中LayoutInflater加载布局总体时序图LayoutInflater源码讲解(api28)LayoutInflater设置Factory2实现方式LayoutInflater源码总结

Activity中LayoutInflater加载布局总体时序图

LayoutInflater源码讲解(api28)

onCreate加载布局,是不是都很熟悉。

@Overrideprotected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_happy);}

AppCompatActivity的onCreate()方法,注意这不是Activity的onCreate方法。

@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();//注意此方法是做换肤的关键。delegate.installViewFactory();delegate.onCreate(savedInstanceState);super.onCreate(savedInstanceState);}

AppCompatDelegatelmpl的installViewFactory方法,AppCompatDelegatelmpl实现了Factory2接口。

public void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {//AppCompatDelegatelmpl实现了Factory2接口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");}}}

LayoutInflater中的setFactory2方法,此方法不允许重复设置值,如果设置值会产生异常,所以如果做动态换肤设置Factory2时,要放在super.onCreate()方法之前,防止异常退出。

public void setFactory2(Factory2 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;//mFactory与mFactory2一块赋值,mFractory2是按照扩展的方法进行开发的。if (mFactory == null) {mFactory = mFactory2 = factory;} else {mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);}}

继续分析setContentView()方法,AppCompatActivity中的setContentView调用的是AppCompatDelegatelmpl的方法。

@Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}

AppCompatDelegatelmpl的setContentView方法,此方法主要是加载我们自定义的布局,将布局添加到容器中。

public void setContentView(int resId) {//主要是初始化根布局,用来存放我们自定义的布局。ensureSubDecor();//存放我们自定义布局的View,此View的类型是FrameLayoutViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();//加载自定义布局,将布局添加到contentParent中。LayoutInflater.from(mContext).inflate(resId, contentParent);mAppCompatWindowCallback.getWrapped().onContentChanged();}

LayoutInflater的inflate方法,此方法中有以后插件化用到的关键代码,此处先留意一下,以后有机会再进行分享插件化相关的技术。

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {//此处是做插件化的关键,activity自定义getResources()方法,用来生产插件对应的资源。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();}}

继续分析inflate方法,其重要流程是创建根布局,然后创建

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");final Context inflaterContext = mContext;final AttributeSet attrs = Xml.asAttributeSet(parser);Context lastContext = (Context) mConstructorArgs[0];mConstructorArgs[0] = inflaterContext;View result = root;try {// Look for the root node.int type;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();if (DEBUG) {System.out.println("**************************");System.out.println("Creating root view: "+ name);System.out.println("**************************");}//处理 merge 标签if (TAG_MERGE.equals(name)) {if (root == null || !attachToRoot) {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//自定义View的根布局,就是自己写的布局的根布局。final View temp = createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params = null;if (root != null) {if (DEBUG) {System.out.println("Creating params from root: " +root);}// Create layout params that match root, if suppliedparams = root.generateLayoutParams(attrs);if (!attachToRoot) {// Set the layout params for temp if we are not// attaching. (If we are, we use addView, below)temp.setLayoutParams(params);}}if (DEBUG) {System.out.println("-----> start inflating children");}// Inflate all children under temp against its context.//把自定义的xml所有除根布局之外的控件全部实例化然后添加进根布局rInflateChildren(parser, temp, attrs, true);if (DEBUG) {System.out.println("-----> done inflating children");}// We are supposed to attach all the views we found (int temp)// to root. Do that now.添加到根布局中if (root != null && attachToRoot) {root.addView(temp, params);}// Decide whether to return the root that was passed in or the// top view found in xml.if (root == null || !attachToRoot) {result = temp;}}} catch (XmlPullParserException e) {final InflateException ie = new InflateException(e.getMessage(), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (Exception e) {final InflateException ie = new InflateException(parser.getPositionDescription()+ ": " + e.getMessage(), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} finally {// Don't retain static reference on context.mConstructorArgs[0] = lastContext;mConstructorArgs[1] = null;Trace.traceEnd(Trace.TRACE_TAG_VIEW);}return result;}}

LayoutInflater的createViewFromTag方法,注意这里有一个BlinkLayout的闪烁小彩蛋,用来闪烁布局。

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();}``//闪烁的菜单,是为了庆祝??? 1995年庆祝什么节日?if (name.equals(TAG_1995)) {// Let's party like it's 1995!return new BlinkLayout(context, attrs);}try {View view;//如果是AppCompatActivity在这初始化,这个可以自己创建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);}//如果是Activity在这初始化。if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {//不带包路径的View,最终都会调用到createView的这个方法if (-1 == name.indexOf('.')) {//这个最终会调用createView(name,“android.view.”,attrs),携带android.view前缀。view = onCreateView(parent, name, attrs);} else {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;}}

我们跟踪一下Layoutlnflater的createView方法,

//利用反射创建对象,为啥不直接new对象呢?因为有些不能访问到?public final View createView(String name, String prefix, AttributeSet attrs)throws ClassNotFoundException, InflateException {Constructor<? extends View> constructor = sConstructorMap.get(name);//鉴别构造方法是否失效,主要为了鉴别类加载器。if (constructor != null && !verifyClassLoader(constructor)) {constructor = null;sConstructorMap.remove(name);}Class<? extends View> clazz = null;try {Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);if (constructor == null) {// Class not found in the cache, see if it's real, and try to add itclazz = 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 constructorif (mFilter != null) {// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);boolean allowed = clazz != null && mFilter.onLoadClass(clazz);mFilterMap.put(name, allowed);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;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 subclassfinal 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);}}

继续分析mFactory2.onCreateView()的方法,其最终会调用到AppCompatDelegatelmpl的createView(),此函数主要对mAppCompatViewInflater进行初始化,然后调用其createView()方法。

public View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs) {if (mAppCompatViewInflater == null) {TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);String viewInflaterClassName =a.getString(R.styleable.AppCompatTheme_viewInflaterClass);if ((viewInflaterClassName == null)|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {// Either default class name or set explicitly to null. In both cases// create the base inflater (no reflection)mAppCompatViewInflater = new AppCompatViewInflater();} else {try {Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);mAppCompatViewInflater =(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor().newInstance();} catch (Throwable t) {Log.i(TAG, "Failed to instantiate custom view inflater "+ viewInflaterClassName + ". Falling back to default.", t);mAppCompatViewInflater = new AppCompatViewInflater();}}}boolean inheritContext = false;if (IS_PRE_LOLLIPOP) {inheritContext = (attrs instanceof XmlPullParser)// If we have a XmlPullParser, we can detect where we are in the layout? ((XmlPullParser) attrs).getDepth() > 1// Otherwise we have to use the old heuristic: shouldInheritContext((ViewParent) parent);}return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */true, /* Read read app:theme as a fallback at all times for legacy reasons */VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */);}

AppCompatViewInflater的createView()来创建View,其最终都转换为AppCompat对应的组件,

final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {final Context originalContext = context;// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy// by using the parent's contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = themifyContext(context, attrs, readAndroidTheme, readAppTheme);}if (wrapContext) {context = TintContextWrapper.wrap(context);}View view = null;// We need to 'inject' our tint aware Views in place of the standard framework versions//这里对View进行转换,自动转换为AppCompat对应的View。switch (name) {case "TextView":view = createTextView(context, attrs);verifyNotNull(view, name);break;case "ImageView":view = createImageView(context, attrs);verifyNotNull(view, name);break;case "Button":view = createButton(context, attrs);verifyNotNull(view, name);break;case "EditText":view = createEditText(context, attrs);verifyNotNull(view, name);break;case "Spinner":view = createSpinner(context, attrs);verifyNotNull(view, name);break;case "ImageButton":view = createImageButton(context, attrs);verifyNotNull(view, name);break;case "CheckBox":view = createCheckBox(context, attrs);verifyNotNull(view, name);break;case "RadioButton":view = createRadioButton(context, attrs);verifyNotNull(view, name);break;case "CheckedTextView":view = createCheckedTextView(context, attrs);verifyNotNull(view, name);break;case "AutoCompleteTextView":view = createAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "MultiAutoCompleteTextView":view = createMultiAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "RatingBar":view = createRatingBar(context, attrs);verifyNotNull(view, name);break;case "SeekBar":view = createSeekBar(context, attrs);verifyNotNull(view, name);break;case "ToggleButton":view = createToggleButton(context, attrs);verifyNotNull(view, name);break;default:// The fallback that allows extending class to take over view inflation// for other tags. Note that we don't check that the result is not-null.// That allows the custom inflater path to fall back on the default one// later in this method.view = createView(context, name, attrs);}//如果不是以上对应的View,则调用以下方法进行创建。if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check its android:onClickcheckOnClickListener(view, attrs);}return view;}

AppCompatViewInflater的createViewFromTag()方法,其与LayoutInflater的createViewFromTag方法有些类似,

private View createViewFromTag(Context context, String name, AttributeSet attrs) {if (name.equals("view")) {name = attrs.getAttributeValue(null, "class");}try {mConstructorArgs[0] = context;mConstructorArgs[1] = attrs;if (-1 == name.indexOf('.')) {//尝试增加不同的前缀进行创建,用反射,如果 反射失败则加载不成功,再重新尝试。for (int i = 0; i < sClassPrefixList.length; i++) {final View view = createViewByPrefix(context, name, sClassPrefixList[i]);if (view != null) {return view;}}return null;} else {return createViewByPrefix(context, name, null);}} catch (Exception e) {// We do not want to catch these, lets return null and let the actual LayoutInflater// tryreturn null;} finally {// Don't retain references on context.mConstructorArgs[0] = null;mConstructorArgs[1] = null;}}

尝试用不同的前缀反射创建View,到了这里View就创建成功了。

private View createViewByPrefix(Context context, String name, String prefix)throws ClassNotFoundException, InflateException {Constructor<? extends View> constructor = sConstructorMap.get(name);try {if (constructor == null) {// Class not found in the cache, see if it's real, and try to add itClass<? extends View> clazz = Class.forName(prefix != null ? (prefix + name) : name,false,context.getClassLoader()).asSubclass(View.class);constructor = clazz.getConstructor(sConstructorSignature);//为了增加速度,这里也尝试了缓存的方式。sConstructorMap.put(name, constructor);}constructor.setAccessible(true);return constructor.newInstance(mConstructorArgs);} catch (Exception e) {// We do not want to catch these, lets return null and let the actual LayoutInflater// tryreturn null;}}

LayoutInflater设置Factory2

在onCreateView中设置Factory2,通过回调函数我们能拿到View的名称(注意,我们连系统的根布局的名称也是可以拿到的),同事也可以拿到其对应的属性值,这样我们就可以根据这些值来创建我们的View,在这里也可以动态的设置我们想要的皮肤。

@Overrideprotected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {//这里主要负责View的创建。@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {Log.e(TAG, "name: " + name);int attributeCount = attrs.getAttributeCount();for (int i = 0; i < attributeCount; i++) {String attributeName = attrs.getAttributeName(i);Log.e(TAG, "attributeName: " + attributeName);}Log.e(TAG, "------------------------------------divide line------------------- ");return null;}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}});super.onCreate(savedInstanceState);setContentView(R.layout.activity_happy);}

日志打印:

name: LinearLayoutattributeName: orientationattributeName: fitsSystemWindowsattributeName: layout_widthattributeName: layout_height------------------------------------divide line------------------- name: ViewStubattributeName: themeattributeName: idattributeName: layoutattributeName: inflatedIdattributeName: layout_widthattributeName: layout_height------------------------------------divide line------------------- name: FrameLayoutattributeName: idattributeName: layout_widthattributeName: layout_heightattributeName: foregroundattributeName: foregroundGravityattributeName: foregroundInsidePadding------------------------------------divide line------------------- name: androidx.appcompat.widget.FitWindowsLinearLayoutattributeName: orientationattributeName: idattributeName: fitsSystemWindowsattributeName: layout_widthattributeName: layout_height------------------------------------divide line------------------- name: androidx.appcompat.widget.ViewStubCompatattributeName: idattributeName: layoutattributeName: inflatedIdattributeName: layout_widthattributeName: layout_height------------------------------------divide line------------------- name: androidx.appcompat.widget.ContentFrameLayoutattributeName: idattributeName: layout_widthattributeName: layout_heightattributeName: foregroundattributeName: foregroundGravity------------------------------------divide line------------------- name: LinearLayoutattributeName: orientationattributeName: layout_widthattributeName: layout_height------------------------------------divide line-------------------

实现方式

我们可以以apk的方式来打包资源,这个apk资源包中只存在资源文件,然后我们通过动态加载技术来加载apk资源包,实现动态换肤的功能。

加载apk资源工具类,此工具类主要是加载资源apk里面的资源引用,这样就可以拿到网络下载来的资源包。

public class SkinResourceManager {private static SkinResourceManager skinManager = new SkinResourceManager();private Context context;private Resources resources;//获取到资源包中的包名private String skinPackageName;private SkinResourceManager(){}public static SkinManager getInstance(){return skinManager;}public void init(Context context){this.context = context;}/*** 根据资源包的 存储路劲去加载资源* @param path*/public void loadSkinApk(String path){//真正的目的是得到资源包的资源对象try {PackageManager packageManager = context.getPackageManager();PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);skinPackageName = packageArchiveInfo.packageName;AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);addAssetPath.setAccessible(true);//在assetManager中执行addAssetPath方法addAssetPath.invoke(assetManager,path);//创建一个资源对象 管理资源包里面的资源resources = new Resources(assetManager,context.getResources().getDisplayMetrics(),context.getResources().getConfiguration());} catch (Exception e) {e.printStackTrace();}}//写匹配各种资源的方法/*** 去匹配颜色* @param resId 需要去匹配ID* @return 匹配到的资源ID(在资源包中的资源ID)*/public int getColor(int resId){if(resourceIsNull()){return resId;}//获取到资源名字和资源类型String resourceTypeName = context.getResources().getResourceTypeName(resId);String resourceEntryName = context.getResources().getResourceEntryName(resId);//去资源包的资源对象中去匹配int identifier = resources.getIdentifier(resourceEntryName,resourceTypeName, skinPackageName);if(identifier == 0){return resId;}return resources.getColor(identifier);}/*** 从资源包中拿到drawable的资源id*/public Drawable getDrawable(int id){if(resourceIsNull()){//获取到这个id在当前应用中的Drawable对象return ContextCompat.getDrawable(context,id);}//获取到资源id的类型String resourceTypeName = context.getResources().getResourceTypeName(id);//获取到的就是资源id的名字String resourceEntryName = context.getResources().getResourceEntryName(id);//就是colorAccent这个资源在外置APK中的idint identifier = resources.getIdentifier(resourceEntryName, resourceTypeName, skinPackageName);if(identifier == 0){return ContextCompat.getDrawable(context,id);}return resources.getDrawable(identifier);}/*** 从资源包中拿到drawable的资源id*/public int getDrawableId(int id){if(resourceIsNull()){return id;}//获取到资源id的类型String resourceTypeName = context.getResources().getResourceTypeName(id);//获取到的就是资源id的名字String resourceEntryName = context.getResources().getResourceEntryName(id);//就是colorAccent这个资源在外置APK中的idint identifier = resources.getIdentifier(resourceEntryName, resourceTypeName, skinPackageName);if(identifier == 0){return id;}return identifier;}public boolean resourceIsNull(){if(resources == null){return true;}return false;}public Resources getResources() {return resources;}}

View的工厂方法,主要用来生成View。

public class SkinFactory implements LayoutInflater.Factory2 {//当Activity继承自AppcompatActivity的时候 去通过这个对象来实例化控件private AppCompatDelegate delegate;private static final String[] prxfixList = {"android.widget.","android.view.","android.webkit."};//缓存构造方法的mapprivate HashMap<String, Constructor<? extends View>> sConstructorMap =new HashMap<String, Constructor<? extends View>>();private Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class};List<SkinView> skinViews = new ArrayList<>();public SkinFactory(AppCompatDelegate delegate){this.delegate = delegate;}@Overridepublic View onCreateView(View view, String s, Context context, AttributeSet attributeSet) {if(s.contains("LinearLayout")){Log.e("MN-------->","1111111111");}//实例化每个控件View crrentView = null;if(delegate !=null){crrentView = delegate.createView(view, s, context, attributeSet);}//兼顾了两种情况 第一种情况就是delegate为空 没有去实例化控件// 第二种情况就是delegate不为空 但是它没有替我们去实例化控件if(crrentView == null){//如果是Activity的情况下 通过反射去讲控件实例化的//1.带包名 2.不带包名if(s.contains(".")){crrentView = onCreateView(s,context,attributeSet);}else{for (String s1 : prxfixList) {String className = s1+s;crrentView = onCreateView(className,context,attributeSet);if(crrentView!=null){break;}}}}//收集需要换肤的控件if(crrentView!=null){paserView(context,crrentView,attributeSet);}return crrentView;}public void apply(){for (SkinView skinView : skinViews) {skinView.apply();}}/*** 收集需要换肤的控件* @param context* @param view* @param attributeSet*/private void paserView(Context context, View view, AttributeSet attributeSet) {TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.Skinable);boolean isKin = typedArray.getBoolean(R.styleable.Skinable_is_skin, false);if(!isKin){return;}//再去找到这个控件需要换肤的属性List<SkinItem> skinItems = new ArrayList<>();for(int x=0;x<attributeSet.getAttributeCount();x++){//属性的名字 类似 textColor srcString attributeName = attributeSet.getAttributeName(x);//如果说符合条件 句证明这条属性是需要换肤的if(attributeName.contains("textColor") || attributeName.contains("src")|| attributeName.contains("background")){//属性的名字 ID 类型String resIdStr = attributeSet.getAttributeValue(x);int resId = Integer.parseInt(resIdStr.substring(1));String resourceTypeName = context.getResources().getResourceTypeName(resId);String resourceEntryName = context.getResources().getResourceEntryName(resId);SkinItem skinItem = new SkinItem(attributeName,resourceTypeName,resourceEntryName,resId);skinItems.add(skinItem);}}if(skinItems.size()>0){SkinView skinView = new SkinView(skinItems,view);skinViews.add(skinView);}}@Overridepublic View onCreateView(String s, Context context, AttributeSet attributeSet) {View view = null;try {//获取到控件的class对象Class aClass = context.getClassLoader().loadClass(s);Constructor<? extends View> constructor;constructor = sConstructorMap.get(s);if(constructor == null){constructor = aClass.getConstructor(mConstructorSignature);sConstructorMap.put(s,constructor);}view = constructor.newInstance(context,attributeSet);} catch (Exception e) {e.printStackTrace();}return view;}public List<SkinView> getSkinViews() {return skinViews;}}

对应的实体类

/*** 每条属性的封装对象*/public class SkinItem {//属性的名字 textColor text backgroundString name;//属性的值的类型 color mipmapString typeName;//属性的值的名字 colorPrimaryString entryName;//属性的资源IDint resId;public SkinItem(String name, String typeName, String entryName, int resId) {this.name = name;this.typeName = typeName;this.entryName = entryName;this.resId = resId;}public String getName() {return name;}public String getTypeName() {return typeName;}public String getEntryName() {return entryName;}public int getResId() {return resId;}}//----------------------------divide line-----------------public class SkinView {//这个控件需要换肤的属性对象的集合List<SkinItem> skinItems;View view;public SkinView(List<SkinItem> skinItems, View view) {this.skinItems = skinItems;this.view = view;}public void apply(){for (SkinItem skinItem : skinItems) {//判断这条属性是background吗?if(skinItem.getName().equals("background")){//1. 设置的是color颜色 2.设置的是图片if(skinItem.getTypeName().equals("color")){//将资源ID 传给ResouceManager 去进行资源匹配 如果匹配到了 就直接设置给控件// 如果没有匹配到 就把之前的资源ID 设置控件view.setBackgroundColor(SkinResourceManager.getInstance().getColor(skinItem.getResId()));}else if(skinItem.getTypeName().equals("drawable") || skinItem.getTypeName().equals("mipmap")){if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN){view.setBackground(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));}else{view.setBackgroundDrawable(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));}}}else if(skinItem.getName().equals("src")){if(skinItem.getTypeName().equals("drawable") || skinItem.getTypeName().equals("mipmap")){//将资源ID 传给ResouceManager 去进行资源匹配 如果匹配到了 就直接设置给控件// 如果没有匹配到 就把之前的资源ID 设置控件((ImageView)view).setImageDrawable(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));}else if(skinItem.getTypeName().equals("color")){//((ImageView)view).setImageResource(SkinManager.getInstance().getColor(skinItem.getResId()));}}else if(skinItem.getName().equals("textColor")){((TextView)view).setTextColor(SkinResourceManager.getInstance().getColor(skinItem.getResId()));}}}public View getView() {return view;}public void setView(View view) {this.view = view;}public List<SkinItem> getSkinItems() {return skinItems;}public void setSkinItems(List<SkinItem> skinItems) {this.skinItems = skinItems;}}

使用,

注意:

需要在Application初始化SkinResourceManager以及加载资源包需要在需要换肤的属性上面注册is_skin="true"属性,这个属于自定义属性,很简单就不在这赘述了。

public class SkinActivity extends AppCompatActivity {public SkinFactory skinFactory;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {skinFactory = new SkinFactory(getDelegate());getLayoutInflater().setFactory2(skinFactory);super.onCreate(savedInstanceState);}public void apply(){skinFactory.apply();}@Overrideprotected void onResume() {super.onResume();skinFactory.apply();}}

LayoutInflater源码总结

Activity与AppCompatActivity都是调用LayoutInflater进行View的创建,但是AppCompatActivity的View是用Factory2进行创建的,我们可以用这种机制来实现替换动态换肤的实现。LayoutInflater的Factory2是带有重设校验的,它是不支持重复设置参数的,我们可以有两种方式来设置我们的Factory2。 在Activity的的surper.onCrate()方法调用setFactory2的方法设置Factory2.通过反射设置Factory2 的值。 在LayoutInflater里面有个1995闪烁layout,我们可以通过在标签中使用blink标签,来达到闪烁布局的效果。创建View是通过反射进行创建的,为了加快构造方法的创建速度,将之前生成的构造方法进行缓存。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。