美文网首页
Android布局流程及换肤原理

Android布局流程及换肤原理

作者: 星宇V | 来源:发表于2020-07-07 19:09 被阅读0次

Android 的布局流程

不考虑AMS binder机制,那么Android 的布局流程的最开始的入口(把前面的当作一个黑盒子,那么后续动作的第一个入口),则是在ActivityThread的performLaunchActivity方法。activity的context也是在这个方法中初始化的。

 ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            //intent中有一些配置信息
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

这里看的是布局的原理,不是启动的原理,所以紧接着,要接着看布局方面的,接着找源码,找到了window的赋值

  Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }

这里能看到,将window赋值为r.mPendingRemoveWindow;
然后接着追r.mPendingRemoveWindow的赋值,能看到它的赋值为

                if (r.activity.mWindowAdded) {
                    if (r.mPreserveWindow) {
                        // Hold off on removing this until the new activity's
                        // window is being added.
                        r.mPendingRemoveWindow = r.window;
                        r.mPendingRemoveWindowManager = wm;
                        // We can only keep the part of the view hierarchy that we control,
                        // everything else must be removed, because it might not be able to
                        // behave properly when activity is relaunching.
                        r.window.clearContentView();
                    } else {
                        wm.removeViewImmediate(v);
                    }
                }

然后紧接着看 r.window的赋值,发现它的值,就是引入了activity中的一个成员变量window

            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();

目前这个属性还未有值,只是拿到这个赋值。
它的值真正的赋值,是在下一步中,及activity的attach中
                appContext.setOuterContext(activity);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback,
                        r.assistToken);

其中这个r指的是ActivityClientRecord,它里面的信息,是在上面提到的黑盒子里初始化的,目前看布局原理就先不考虑这个了。
在activity的attach方法中,可以看到mWindow的赋值
   mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

然后我们看布局放上去的方法,一般在activity的oncreate中的setcontentview中,点进去发现它调用的方法为

 public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
而getwindow即mWindow,也就是PhoneWindow
所以,查看它的相应方法
@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
在第一步,进行了installDecor()跟进去接着看
 private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
          .....
          .....

然后点进generateLayout(mDecor)
它就是各种操作,根据不同值得到不同的layoutResource(系统内置的一些)然后加载,假如得到一个layoutResource之后,则会

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

在进入,就能看到
  mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {

            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();

这个layoutResource还不是我们自己定义的layout,这是系统匹配出来的一个layoutResource
然后在通过addview的方式,也就把刚刚文件上的布局,放到了DecorView上了

**所以activity一打开,首先展示的就是这个layout**

其实看源码也相当于看一个树形结构,从一个分叉深入进去后,想要顺序看,还是得接着回到刚刚引入深入点的那个位置
然后将源码回退到phoneWindow中,接着往下看。看到

然后通过刚刚那一系列深入,能看出mContentParent其实也就是那个layout中的content。
 mLayoutInflater.inflate(layoutResID, mContentParent);
然后点入inflate方法进去,看到很多inflate方法,但是最终,他都是会调到这个inflate里,即
 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
所以,我们这里的root也就是刚刚传入过来的content控件

所以,整体结构相当于


image.png

然后在inflate方法中,接着看

// Temp is the root view that was found in the xml
                   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 supplied
                        params = 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);
                        }
                    }
attachToRoot这个参数,表示你是写代码add进去的view,还是从xml解析中进入的。通常写代码add的都是ture,从xml中进入的是false,所以,当为false的时候,会把params设置进去。

接着看createViewFromTag方法是怎么创建temp这个view的

 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();
        }

        try {
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

onCreateView里,最后创建调用的还是createView

紧接着看createview中

Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
        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 it
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, viewContext, 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 = Class.forName(prefix != null ? (prefix + name) : name, false,
                                mContext.getClassLoader()).asSubclass(View.class);

                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, viewContext, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
            }


首先 clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);是通过名字找到的clazz
然后在 constructor = clazz.getConstructor(mConstructorSignature);
static final Class<?>[] mConstructorSignature = new Class[] {
            Context.class, AttributeSet.class};
所以,在自定义控件中,这个两个参数的构造函数一定不能少,它的初始化都是调用了这个两个参数的方法。随后就把这个构造方法和名字缓存起来了,放入了sConstructorMap


然后在这个方法的下面(源码没放全,毕竟都挺多的,放进去通篇都是源码了)
         try {
                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]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
通过newInstance得到这个view,然后进行return

所以,如果要实现换肤,这里是一条思路,可以在其中做些事情,通过自定义layoutInfalter,在createView方法中进行某些操作
还有一处,在createViewFromTag方法中,在view==null判断前,还有View view = tryCreateView(parent, name, context, attrs);操作,这个方法里用到了各种factory

 public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        View 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);
        }

        return view;
    }
从中可以很清楚的看到,当我们设置了factory或者factory2后(这俩工厂2继承1,然后2中的参数带有父控件,1不带),就可以直接对view进行初始化,也就可以隔断前面创建view的过程了。

所以,我们也可以通过设置factory来完成换肤,工厂内的oncreateview方法,抄源码然后做响应的自己需求的修改就好了。记得这个工厂的设置要放在setcontentview之前。很显而易见。。。
所以,View temp的创建这里,可以延伸出两种换肤方案。

资源文件

在C++层,会干一个事情,生成一个resource.arsc文件,类似于数据库文件,整个这个文件是一个二进制的文件。在这个文件中,标示了res这个路径下的每一个资源所对应的信息。


image.png

所以在我们加载一个apk包的时候,会有一个类去负责读这个文件的信息和这个res目录下的信息。
现在接着回ActivityThread里。。。找到handleBindApplication方法,找到资源加载的过程都在什么时间点

首先注意到这个仪表信息类,这个东西还是很重要的。很多事情要通过它来完成
 mInstrumentation = new Instrumentation();
 mInstrumentation.basicInit(this);


然后初始化application
// Allow disk access during application and provider setup. This could
        // block processing ordered broadcasts, but later processing would
        // probably end up doing the same disk access.
        Application app;
        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
        final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
        try {
            // If the app is being launched for full backup or restore, bring it up in
            // a restricted environment with the base application class.
            app = data.info.makeApplication(data.restrictedBackupMode, null);

            // Propagate autofill compat state
            app.setAutofillOptions(data.autofillOptions);

            // Propagate Content Capture options
            app.setContentCaptureOptions(data.contentCaptureOptions);

            mInitialApplication = app;

            // don't bring up providers in restricted mode; they may depend on the
            // app's custom Application class
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);
                }
            }

            // Do this after providers, since instrumentation tests generally start their
            // test thread at this point, and we don't want that racing.
            try {
                mInstrumentation.onCreate(data.instrumentationArgs);
            }
           catch (Exception e) {
                throw new RuntimeException(
                    "Exception thrown in onCreate() of "
                    + data.instrumentationName + ": " + e.toString(), e);
            }
            try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                if (!mInstrumentation.onException(app, e)) {
                    throw new RuntimeException(
                      "Unable to create application " + app.getClass().getName()
                      + ": " + e.toString(), e);
                }
            }
        } finally {
            // If the app targets < O-MR1, or doesn't change the thread policy
            // during startup, clobber the policy to maintain behavior of b/36951662
            if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1
                    || StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
                StrictMode.setThreadPolicy(savedPolicy);
            }
        }

将application初始化之后,然后通过仪表类mInstrumentation调用它的oncreate方法 mInstrumentation.callApplicationOnCreate(app);

从创建application中点进去

 ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
这里先初始化来一个contextImpl,接着点进去


static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
            String opPackageName) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null, opPackageName);
        context.setResources(packageInfo.getResources());
        return context;
    }

在这里有一句很重要的话
context.setResources(packageInfo.getResources());
从包信息中获取资源,也就是从apk中去拿资源

然后接着看里面具体是怎么获取资源的,点进去(LoadedApk类的getResources方法)

 public Resources getResources() {
        if (mResources == null) {
            final String[] splitPaths;
            try {
                splitPaths = getSplitPaths(null);
            } catch (NameNotFoundException e) {
                // This should never fail.
                throw new AssertionError("null split not found");
            }

            mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader());
        }
        return mResources;
    }

然后从mResources的初始化中接着进去(到了ResourceManager类的getResources方法)

 public @Nullable Resources getResources(@Nullable IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

重要的是 return getOrCreateResources(activityToken, key, classLoader);获取或者创建资源,再点进去
找到再往下的一个入口
 final ActivityResources activityResources =
                        getOrCreateActivityResourcesStructLocked(activityToken);


再深入


  private ActivityResources getOrCreateActivityResourcesStructLocked(
            @NonNull IBinder activityToken) {
        ActivityResources activityResources = mActivityResourceReferences.get(activityToken);
        if (activityResources == null) {
            activityResources = new ActivityResources();
            mActivityResourceReferences.put(activityToken, activityResources);
        }
        return activityResources;
    }

(看源码中,看到在集合类里去取我们需要的东西,这个集合类刚开始一般都为空,一个套路,所以可以先暂时不看这个,往下看)


                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                }


发现resourcesImpl还是从集合中去找,紧接着继续往下,找到了其初始化的位置


  // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }

此处通过放入一个key来创建了资源,这个key暂时还不知道是干什么的,然后我们接着深入这个方法里面去查看


   private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }

会发现它里面给我们造了一个AssetManager,然后接着进入AssetManager的初始化中createAssetManager(key);

 /**
     * Creates an AssetManager from the paths within the ResourcesKey.
     *
     * This can be overridden in tests so as to avoid creating a real AssetManager with
     * real APK paths.
     * @param key The key containing the resource paths to add to the AssetManager.
     * @return a new AssetManager.
    */
    @VisibleForTesting
    @UnsupportedAppUsage
    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        final AssetManager.Builder builder = new AssetManager.Builder();

        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
            try {
                builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
                        false /*overlay*/));
            } catch (IOException e) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }

        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                try {
                    builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,
                            false /*overlay*/));
                } catch (IOException e) {
                    Log.e(TAG, "failed to add split asset path " + splitResDir);
                    return null;
                }
            }
        }

        if (key.mOverlayDirs != null) {
            for (final String idmapPath : key.mOverlayDirs) {
                try {
                    builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
                            true /*overlay*/));
                } catch (IOException e) {
                    Log.w(TAG, "failed to add overlay path " + idmapPath);

                    // continue.
                }
            }
        }

        if (key.mLibDirs != null) {
            for (final String libDir : key.mLibDirs) {
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    try {
                        builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
                                false /*overlay*/));
                    } catch (IOException e) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");

                        // continue.
                    }
                }
            }
        }

        return builder.build();
    }
 builder.addApkAssets这个api的调用,就帮我们实现了资源文件的路径的加载(里面就是一个数据结构列表的添加,但是通过构造ApkAssets对象,其内部是native实现)
 * The main implementation is native C++ and there is very little API surface exposed here. The APK
 * is mainly accessed via {@link AssetManager}.
它中间最重要的是ApkAssets这个类,从这个类的注视中也能看出,它主要有native实现,通过构造方法中在进入nativeLoad

所以,也能得出,这个apk加载好以后,生成resources.arsc文件。所以换肤的策略,也可以使用assceManager这个api进行这个皮肤apk路径的添加,他就会把这个apk里的resources.arsc以及res信息加载进我们的apk中去
所以,我们的app就相当于一个宿主apk,然后皮肤的apk就相当于插件apk。(addApkAssets这个方法中加入插件apk相关信息)

它的整体封装结构为

image.png
所以,我们平常在使用resource的时候,其内部就是调用到resourcesImpl里,然后调到mAssets这个量里面的方法。
所以我们AssetManager只干一个事情,就是它通过resource.arsc这个表找到对应的资源,然后再通过读流去取这些数据。所以ResourceImpl Resources相对AssetManager都是一个包装,包着它,顺便加了一些其他数据。从AssetManager内的一些加载方法中可以看出,它根据一些信息去获取这些资源,那么我们也就可以使用它来实现换肤的一些操作

所以,可以看出换肤的基本步骤为

  1. 制作皮肤包APK
  2. 收集XML数据
  • 利用view的生产对象的过程Factory2接口(注意setFactory2中的实现,默认这个设置只能用一次,其内部的一个标识就会改变。所以可以通过反射去修改它的这个标识)
  1. 记录需要换肤的属性
  2. 读取皮肤包内容(记得先加载进来)不同版本,具体的api操作变了,记得区分
  3. 执行换肤操作

相关文章

网友评论

      本文标题:Android布局流程及换肤原理

      本文链接:https://www.haomeiwen.com/subject/scawqktx.html