Android切換皮膚原理的一些理解

前言

參照了Android-skin-support 這個(gè)開源庫(kù),通過閱讀了這個(gè)開源庫(kù),進(jìn)行了學(xué)習(xí),總結(jié)出來的筆記

基本的使用方式,其實(shí)框架的github里面講的挺清楚了。

1、引入庫(kù)

我這邊還是support包,暫時(shí)沒用androidx,所以導(dǎo)入的是support

implementation 'skin.support:skin-support:3.1.4'                   // skin-support 基礎(chǔ)控件支持
implementation 'skin.support:skin-support-design:3.1.4'            // skin-support-design material design 控件支持[可選]
implementation 'skin.support:skin-support-cardview:3.1.4'          // skin-support-cardview CardView 控件支持[可選]
implementation 'skin.support:skin-support-constraint-layout:3.1.4' // skin-support-constraint-layout ConstraintLayout 控件支持[可選]

在Application的onCreate中初始化

@Override
public void onCreate() {
    super.onCreate();
    SkinCompatManager.withoutActivity(this)                         // 基礎(chǔ)控件換膚初始化
            .addInflater(new SkinMaterialViewInflater())            // material design 控件換膚初始化[可選]
            .addInflater(new SkinConstraintViewInflater())          // ConstraintLayout 控件換膚初始化[可選]
            .addInflater(new SkinCardViewInflater())                // CardView v7 控件換膚初始化[可選]
            .setSkinStatusBarColorEnable(false)                     // 關(guān)閉狀態(tài)欄換膚,默認(rèn)打開[可選]
            .setSkinWindowBackgroundEnable(false)                   // 關(guān)閉windowBackground換膚,默認(rèn)打開[可選]
            .loadSkin();
}

在BaseActivity里面使用這個(gè)SkinAppCompatDelegateImpl這個(gè)方法來代理源碼中的創(chuàng)建的AppCompatDelegate。

@NonNull
@Override
public AppCompatDelegate getDelegate() {
    return SkinAppCompatDelegateImpl.get(this, this);
}

補(bǔ):這里補(bǔ)充一下自己的理解。這個(gè)代理,直接其實(shí)就是讓AppCompatDelegate # installViewFactory 方法,延遲導(dǎo)入LayoutInflater,通過實(shí)現(xiàn)Application.ActivityLifecycleCallbacks的 activity 生命周期方法來注入LayoutInflater。

//android.view.LayoutInflater.java
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 = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

如果在一開始設(shè)置上去了,就會(huì)拋出"A factory has already been set on this LayoutInflater"異常,或者通過反射的方式先重置了mFactorySet為false,然后重新設(shè)置上自己的LayoutInflater。


//assets方式切換皮膚
SkinCompatManager.getInstance().loadSkin("night.skin", null, SkinCompatManager.SKIN_LOADER_STRATEGY_ASSETS);
//...zip等方式加載皮膚都可以自己設(shè)置

//重置為原皮膚
SkinCompatManager.getInstance().restoreDefaultTheme();

這樣就可以把換膚框架使用起來了。

一、為什么要換膚,什么叫換膚

個(gè)人理解:讓用戶體驗(yàn)會(huì)更好

換膚:就是認(rèn)為動(dòng)態(tài)的替換資源(文字、顏色、字體大小、圖片,布局文件…),例如使用View的setBackgroundResource,setTextSize等函數(shù)

上面可以提取2個(gè)問題

  1. 換膚資源怎么獲取
  2. 換膚資源怎么設(shè)置

二、換膚資源怎么設(shè)置

基于api 26

主角就是LayoutInfater,xml布局獲取的加載。

為什么是 LayoutInfater呢。換膚就是能拿到換膚控件的對(duì)象,然后進(jìn)行調(diào)用 setTextSize、setBackgroundResource,setImageResource,setTextColor等。關(guān)鍵是這么拿到這些換膚的控件對(duì)象。

LayoutInflater 獲取方式

LayoutInfater是個(gè)抽象類,最終發(fā)現(xiàn)是 PhoneLayoutInfater LayoutInfater -》 PhoneLayoutInfater

怎么知道的呢?那就我們?cè)趺传@取LayoutInflater這么獲取的,獲取方式:

  1. Activity # getLayoutInflater() -> 最終是在PhoneWindow的成員變量,這個(gè)成員變量是在PhoneWindow構(gòu)造方法調(diào)用的,最終其實(shí)還是context.getSystemService的方式來獲取的
@UnsupportedAppUsage
public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}
  1. fragment # getLayoutInflater() -> 點(diǎn)擊源碼看的時(shí)候,最終還是通過
LayoutInflater LayoutInflater =
        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  1. LayoutInflater.from(context)

補(bǔ)充:我們常用的View.inflate加載布局,其實(shí)內(nèi)部也是通過LayoutInflater.from()

總結(jié):最終還是通過context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)來進(jìn)行獲取的LayoutInflater對(duì)象

看了一下getSystemService這個(gè)方法在Context里面,那就直接看ContextImpl.java的getSystemService方法

@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

SystemServiceRegistry類的注釋上說明

Manages all of the system services that can be returned by {@link Context#getSystemService}. * Used by {@link ContextImpl}.

管理的所有的服務(wù)提供給用戶使用

注:仿照這個(gè)類應(yīng)該可以自己弄一個(gè)app的aidl管理服務(wù)類

/**
 * Gets a system service from a given context.
 */
public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
}

上面方法可以知道,服務(wù)是根據(jù)上下文來進(jìn)行獲取的

在注冊(cè)服務(wù)的時(shí)候,這里實(shí)現(xiàn)了

registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
        new CachedServiceFetcher<LayoutInflater>() {
    @Override
    public LayoutInflater createService(ContextImpl ctx) {
        return new PhoneLayoutInflater(ctx.getOuterContext());
    }});

差不多了,我們已經(jīng)知道了2點(diǎn)

1)我們的LayoutInflater是PhoneLayoutInflater

2)不同的Context都會(huì)創(chuàng)建一個(gè)PhoneLayoutInflater

LayoutInfater#inflate 使用,以及實(shí)現(xiàn)創(chuàng)建View的過程源代碼查看

回過頭來,我們獲取LayoutInflater之后通常會(huì)進(jìn)行

inflate(int resource, ViewGroup root, boolean attachToRoot)

然后進(jìn)行如下步驟:

inflater() -> createViewFromTag -> createViewFromTag(5 params) -> tryCreateView -> Factory.onCreateView

inflater()中

XmlResourceParser parser = res.getLayout(resource);

//獲取屬性
final AttributeSet attrs = Xml.asAttributeSet(parser);

通過PULL解析獲取屬性(layout_height="-1", layout_width="-1"等)

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                           boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
        //...略
        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;
        }
            //...略

        return view;
    }

這里最終調(diào)用了 mFactory2 這個(gè)對(duì)象,這個(gè)對(duì)象的默認(rèn)實(shí)現(xiàn),我們回過頭來看AppCompatActivity里面的

//android.support.v7.app.AppCompatActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    
    delegate.installViewFactory();
    
    delegate.onCreate(savedInstanceState);
    if (delegate.applyDayNight() && mThemeId != 0) {
        if (Build.VERSION.SDK_INT >= 23) {
            onApplyThemeResource(getTheme(), mThemeId, false);
        } else {
            setTheme(mThemeId);
        }
    }
    super.onCreate(savedInstanceState);
}

通過 installViewFactory 這個(gè)方法調(diào)用了factory的設(shè)置

//android.support.v7.app.AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    }
}

LayoutInflaterCompat.setFactory2(layoutInflater, factory)的第二個(gè)參數(shù)就是需要實(shí)現(xiàn) LayoutInflater.Factory2 的對(duì)象,這里傳的是this,所以說看AppCompatDelegateImplV9的具體實(shí)現(xiàn)邏輯。

//android.support.v7.app.AppCompatDelegateImplV9.java
/**
 * From {@link LayoutInflater.Factory2}.
 */
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    // First let the Activity's Factory try and inflate the view
    final View view = callActivityOnCreateView(parent, name, context, attrs);
    if (view != null) {
        return view;
    }

    // If the Factory didn't handle it, let our createView() method try
    return createView(parent, name, context, attrs);
}

這里的到了一個(gè)新的發(fā)現(xiàn),我覺得這個(gè)callActivityOnCreateView能進(jìn)行皮膚緩存所有View,就是在activity里面實(shí)現(xiàn)onCreateView()方法,結(jié)果好像不打印子View了,不清楚什么原因 callActivityOnCreateView ->

看 createView 這個(gè)方法

//android.support.v7.app.AppCompatDelegateImplV9.java
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    
    if (mAppCompatViewInflater == null) {
        mAppCompatViewInflater = new AppCompatViewInflater();
    }
    //...

    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:就是用來把AppCompatTextView AppCompatImageView等AppCompatXXView系列的進(jìn)行替換創(chuàng)建??碅ppCompatViewInflater # createView

//android.support.v7.app.AppCompatViewInflater
public final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        
        //...略
    View view = null;

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = new AppCompatTextView(context, attrs);
            break;
        case "ImageView":
            view = new AppCompatImageView(context, attrs);
            break;
            //...略
    }

    if (view == null && originalContext != context) {
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        // If we have created a view, check its android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}

如果不符合AppCompatXXView類的就只能進(jìn)行反射去創(chuàng)建了,看createViewFromTag方法

//android.support.v7.app.AppCompatViewInflater

private static final String[] sClassPrefixList = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };


private View createViewFromTag(Context context, String name, AttributeSet attrs) {
        //...略
    try {
        mConstructorArgs[0] = context;
        mConstructorArgs[1] = attrs;

        //1 
        if (-1 == name.indexOf('.')) {
            for (int i = 0; i < sClassPrefixList.length; i++) {
                final View view = createView(context, name, sClassPrefixList[i]);
                if (view != null) {
                    return view;
                }
            }
            return null;
        } else {
         //2
            return createView(context, name, null);
        }
    }
    //...略
}

private View createView(Context context, String name, String prefix)
            throws ClassNotFoundException, InflateException {
        //..略
        //通過 2個(gè)參數(shù)的構(gòu)造方法創(chuàng)建View對(duì)象
        Class<? extends View> clazz = context.getClassLoader().loadClass(
                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                //...略
        constructor = clazz.getConstructor(sConstructorSignature);
        constructor.setAccessible(true);
        return constructor.newInstance(mConstructorArgs);
    }

1 如果是沒帶點(diǎn)的,就是像TextView等其他的系統(tǒng)類,通是sClassPrefixList這個(gè)數(shù)組遍歷拼接上,進(jìn)行反射創(chuàng)建View對(duì)象。

2 帶了點(diǎn)的,就是一些三方的v7,v4,自定義View,都會(huì)帶上包名,所以走第二步這個(gè)方法

以上,就基本知道View怎么來的了,換膚的話需要進(jìn)行,把所有需要換膚的View保存起來,然后在換皮膚的時(shí)候,進(jìn)行調(diào)用 setTextColor、setBackgroundResource, setImageResource 等。

不妨自己去代理View的創(chuàng)建過程,然后通過自己去實(shí)現(xiàn)一些View對(duì)應(yīng)布局的View,這樣類似 AppCompatXXView 這種變成自己的,如:SkinCompatXXView 這樣,這些View都自己掌控了,加個(gè)換膚的方法 applySkin 就好實(shí)現(xiàn)了。

三、換膚資源怎么獲取

上面以及得到了,所有的 可以進(jìn)行具有換膚的View 對(duì)象

換膚對(duì)象:其實(shí)就是自己通過實(shí)現(xiàn)Factory2,然后在后續(xù) LayoutInflater 流程

inflater() -> createViewFromTag -> createViewFromTag(5 params) -> tryCreateView -> Factory.onCreateView

最終調(diào)用自己這個(gè)Factory2#onCreateView(View, String, Context, AttributeSet),創(chuàng)建對(duì)應(yīng)的SkinCompatXXView,在這過程中,我們可以順便把這些對(duì)象緩存起來,為后續(xù)的資源獲取后,進(jìn)行調(diào)用setTextColor、setBackgroundResource, setImageResource 等做準(zhǔn)備

方式一、加載assets下,通過apk打包好的資源包

//skin.support.load.SkinAssetsLoader
private String copySkinFromAssets(Context context, String name) {
    String skinPath = new File(SkinFileUtils.getSkinDir(context), name).getAbsolutePath();
    try {
        InputStream is = context.getAssets().open(
                SkinConstants.SKIN_DEPLOY_PATH + File.separator + name);
        OutputStream os = new FileOutputStream(skinPath);
        int byteCount;
        byte[] bytes = new byte[1024];
        while ((byteCount = is.read(bytes)) != -1) {
            os.write(bytes, 0, byteCount);
        }
        os.close();
        is.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return skinPath;
}

//skin.support.utils.SkinFileUtils
public static String getSkinDir(Context context) {
        File skinDir = new File(getCacheDir(context), SkinConstants.SKIN_DEPLOY_PATH);
        if (!skinDir.exists()) {
            skinDir.mkdirs();
        }
        return skinDir.getAbsolutePath();
}

copySkinFromAssets 就是獲取本地 assets下面的皮膚資源,然后拷貝到 cache目錄下面

cache目錄有兩種情況

  1. 掛載sdcard的情況,且創(chuàng)建成功了,就是 sdcard/Android/data/${packageName}/skins下面
  2. sdcard掛載失敗了或者沒成功創(chuàng)建,就是在 data/data/${packageName}/skins 目錄下面

皮膚資源已經(jīng)放到了對(duì)應(yīng)的目錄,然后處理這個(gè)文件

//skin.support.SkinCompatManager
/**
 * 獲取皮膚包包名.
 *
 * @param skinPkgPath sdcard中皮膚包路徑.
 * @return
 */
public String getSkinPackageName(String skinPkgPath) {
    PackageManager mPm = mAppContext.getPackageManager();
    PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
    return info.packageName;
}

/**
 * 獲取皮膚包資源{@link Resources}.
 *
 * @param skinPkgPath sdcard中皮膚包路徑.
 * @return
 */
@Nullable
public Resources getSkinResources(String skinPkgPath) {
    try {
        PackageInfo packageInfo = mAppContext.getPackageManager().getPackageArchiveInfo(skinPkgPath, 0);
        packageInfo.applicationInfo.sourceDir = skinPkgPath;
        packageInfo.applicationInfo.publicSourceDir = skinPkgPath;
        Resources res = mAppContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
        Resources superRes = mAppContext.getResources();
        return new Resources(res.getAssets(), superRes.getDisplayMetrics(), superRes.getConfiguration());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

通過上面的方法,獲取到了皮膚資源中的Resources對(duì)象,后面換皮膚的時(shí)候,就通過這個(gè)對(duì)象,獲取里面的皮膚資源

參考

http://www.itdecent.cn/p/f0f3de2f63e3

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容