Android 动态换肤 支持5.0-9.0系统,支持自定义view

本文详细介绍了Android应用程序中动态换肤和插件化换肤的实现方法,包括理解View的加载流程、自定义Factory2加载View、通过反射修改资源路径以及创建SkinManager管理类来替换资源。通过这些步骤,可以在不增加APK体积的情况下实现在运行时无缝切换应用主题。此外,还展示了如何在BaseActivity中应用这些技术,并提供了测试代码以展示换肤效果。
摘要由CSDN通过智能技术生成

动态换肤,插件化换肤的好处:不会增加apk包体积。无缝换肤。不用退出应用。不会闪屏。

思路:

1.如果要更换view的背景颜色,字体颜色,图片等,就必须先知道view的加载流程。

2.知道view的加载之后,就可以setTextColor,或者setBackroudColor了。

3.主要是setTextColor或者setBackroudColor的资源加载哪个地方的。默认是加载内置app

的。我们要做的事情就是换肤之后加载我们特定的换肤资源。

 

解决问题1:

activity里面view的加载流程。加载xml里面的view的过程。

默认使用的是LayoutInflater内部接口Factory里面的onCreateView进行创建view的。

系统提供了我们自定义加载view的Factory2。也是在LayoutInflater里面的一个接口。

 

activity默认使用Factory加载view。如果要使用Factory2加载view就得

在activity 的onCreate()方法中的  super.onCreate(savedInstanceState)之前调用

LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);方法进行设置Factory2

因为activity实现了LayoutInflater.Factory2接口。所以可以传入this。

 

接下来就是重写activity的 onCreateView方法。

activity默认使用 AppCompatViewInflater 类的 createView()方法去创建view。

现在我们要去拦截默认创建view的过程。只要继承AppCompatViewInflater 写一个子类

然后根据判断条件创建自己的view就可以把控view的颜色背景等的更换。

这里的name和attrs属性是为了后面找resourceId 和resourceName和resourceType用的。

里面的SkinnableTextView,SkinnableButton...是自己定义的目的是兼容

当我们需要换肤的时候,调用skinnableView()这个方法就可以换肤了。

问题:

这个换肤的思路:

首先我们要自定义一些attts属性。方便我们通过resourceId去获取Resource。

上面这个类是存储attrs属性的。我们取属性的时候从这个类里面取就可以了。

在创建view的方法里面把AttrsBean对象也创建并且赋值。

attrs文件里面具体的参数:下面只是一部分,后面可以自己添加。想要换Text string也是可以的。

 

需要换肤的view都去实现

ViewsMatch接口。这个接口只有一个方法。
public interface ViewsMatch {

    void skinnableView();
}

 

下面重点:

如何加载改变资源的路径??:

解决:

通过 AssetManager 里面的addAssetPath(String)方法进行资源路径的改变。

下面利用反射进行调用。

//创建资源管理器
AssetManager assetManager = AssetManager.class.newInstance();
//被@hide,目前只能反射去拿这个方法。 如果担心这个方法被@hide 可以反射addAssetPathInternal()方法。源码366行 + 387行
Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
//设置私有方法可访问
addAssetPath.setAccessible(true);
//执行addAssetPath方法
addAssetPath.invoke(assetManager, skinPath);

下面是SkinManager 类的代码:

package com.example.skinlibrary;

import android.app.Application;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public final class SkinManager {

    private static SkinManager instance;
    private Application application;
    private Resources appResources;//app内置资源
    private Resources skinResources;//皮肤包的资源
    private String skinPackageName;//皮肤包的包名
    private boolean isDefaultSkin = true;//是否默认皮肤(app内置资源)
    private static final String ADD_ASSET_PATH = "addAssetPath";//方法名
    private Map<String ,SkinCache> cacheSkin;

    private SkinManager(Application application) {
        this.application = application;
        appResources = application.getResources();
        cacheSkin = new HashMap<>();
    }

    public static void init(Application application) {
        if (instance == null) {
            synchronized (SkinManager.class) {
                if (instance == null) {
                    instance = new SkinManager(application);
                }
            }
        }
    }

    public static SkinManager getInstance() {
        return instance;
    }

    /**
     * 加载皮肤包资源
     *
     * @param skinPath 皮肤包路径,为空则加载app内置资源
     */
    public void loaderSkinResources(String skinPath) {

        if (TextUtils.isEmpty(skinPath)) {
            isDefaultSkin = true;
            return;
        }

        if (cacheSkin.containsKey(skinPath)) {
            isDefaultSkin = false;
            SkinCache skinCache = cacheSkin.get(skinPath);
            if (skinCache!=null){
                skinPackageName = skinCache.getSkinPackageName();
                skinResources = skinCache.getSkinResources();
                return;
            }
        }

        try {
            //创建资源管理器
            AssetManager assetManager = AssetManager.class.newInstance();
            //被@hide,目前只能反射去拿这个方法。 如果担心这个方法被@hide 可以反射addAssetPathInternal()方法。源码366行 + 387行
            Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
            //设置私有方法可访问
            addAssetPath.setAccessible(true);
            //执行addAssetPath方法
            addAssetPath.invoke(assetManager, skinPath);


            //创建加载外部皮肤包资源
            skinResources = new Resources(assetManager,
                    appResources.getDisplayMetrics(), appResources.getConfiguration());

            //根据皮肤包文件,获取皮肤包应用的包名
            skinPackageName = application.getPackageManager()
                    .getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
            //如果无法获取皮肤包应用的包名,加载app内置资源
            //如果为空使用app内置资源,否则使用皮肤包。
            isDefaultSkin = TextUtils.isEmpty(skinPackageName);

            if (!isDefaultSkin){
                cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
            }

        } catch (Exception e) {
            e.printStackTrace();
            //预判:通过api加载可能有异常
            isDefaultSkin = true;
        }
    }


    /**
     * 参考:resources.arsc 资源映射表
     * 通过ID值获取资源的name和type
     *
     * @param resourceId 资源的ID值(app内置资源)
     * @return 如果没有皮肤包则加载内置app资源id,反之则加载皮肤包对应的资源id
     */
    private int getSkinResourceIds(int resourceId) {
        //优化
        if (isDefaultSkin) return resourceId;

        String resourceName = appResources.getResourceEntryName(resourceId);// 资源的名字
        String resourceTypeName = appResources.getResourceTypeName(resourceId);//drawable color

        int skinResourceId = skinResources.getIdentifier(resourceName, resourceTypeName, skinPackageName);
        //skinResourceId == 0证明没有在皮肤包里面找到这个资源。没找到就使用默认的。
        isDefaultSkin = skinResourceId == 0;

        return skinResourceId == 0 ? resourceId : skinResourceId;
    }

    public boolean isDefaultSkin() {
        return isDefaultSkin;
    }


    //=============================================================================================
    public int getColor(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return isDefaultSkin ? appResources.getColor(ids) : skinResources.getColor(ids);
    }

    public ColorStateList getColorStateList(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return isDefaultSkin ? appResources.getColorStateList(ids) : skinResources.getColorStateList(ids);
    }

    //mipmap和darwable统一用法
    public Drawable getDrawableOrMipMap(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return isDefaultSkin ? appResources.getDrawable(ids) : skinResources.getDrawable(ids);
    }

    public String getString(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return isDefaultSkin ? appResources.getString(ids) : skinResources.getString(ids);
    }

    //返回特殊情况,可能是color drawable / mipmap
    public Object getBackroudOrSrc(int resourceId) {
        //需要获取当前属性的类型名,Resource.getResourceTypeName(resourceId)再判断
        String resourceTypeName = appResources.getResourceTypeName(resourceId);
        switch (resourceTypeName) {
            case "color":
                return getColor(resourceId);
            case "mipmap":
            case "drawable":
                return getDrawableOrMipMap(resourceId);
        }
        return null;
    }
}

这个管理类就可以替换资源了。在application里面初始化这个类。

public class BaseApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        SkinManager.init(this);

    }
}

资源的路径是我们插件化创建新的apk的路径。新apk里面的res资源和原始app里面的资源名字要一一对应。

要换肤的就对应起来。不用换肤的不对应也没事。

下面是baseActivity的代码:

package com.example.skinlibrary;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.view.LayoutInflaterCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.skinlibrary.core.CustomAppCompatViewInflater;
import com.example.skinlibrary.utils.ActionBarUtils;
import com.example.skinlibrary.utils.NavigationBarUtils;
import com.example.skinlibrary.utils.StatusBarUtils;


public class SkinActivity extends AppCompatActivity implements UseSkin {
    CustomAppCompatViewInflater mCustomAppCompatViewInflater;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
        super.onCreate(savedInstanceState);
    }

    @Override
    public boolean useSkin() {
        return false;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        if (useSkin()) {
            if (mCustomAppCompatViewInflater == null) {
                mCustomAppCompatViewInflater = new CustomAppCompatViewInflater(context);
            }
            mCustomAppCompatViewInflater.setName(name);
            mCustomAppCompatViewInflater.setAttrs(attrs);
            return mCustomAppCompatViewInflater.autoMatch();
        }

        return super.onCreateView(name, context, attrs);
    }

    protected void setDayNightMode(@AppCompatDelegate.NightMode int nightMode) {
        final boolean isPost21 = Build.VERSION.SDK_INT >= 21;
        getDelegate().setLocalNightMode(nightMode);
        if (isPost21) {
            //换状态栏颜色
            StatusBarUtils.forStatusBar(this);
            //换标题栏颜色
            ActionBarUtils.forActionBar(this);

            //换导航栏颜色
            NavigationBarUtils.forNavigationBar(this);
        }

        View decorView = getWindow().getDecorView();
        applyDayNightForView(decorView);
    }

    /**
     * 动态换肤(api限制5.0版本)
     *
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    protected void defaultSkin(int temeColorId) {
        this.skinDynamic(null, temeColorId);
    }


    /**
     * 动态换肤(api限制5.0版本)
     *
     * @param skinPath
     * @param themeColorId
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    protected void skinDynamic(String skinPath, int themeColorId) {
        SkinManager.getInstance().loaderSkinResources(skinPath);
        if (themeColorId != 0) {
            int temeColorId = SkinManager.getInstance().getColor(themeColorId);
            //换状态栏颜色
            StatusBarUtils.forStatusBar(this, temeColorId);
            //换标题栏颜色
            ActionBarUtils.forActionBar(this, temeColorId);

            //换导航栏颜色
            NavigationBarUtils.forNavigationBar(this, temeColorId);

            View decorView = getWindow().getDecorView();
            applyDayNightForView(decorView);
        }
    }


    /**
     * 回调接口 给具体控件换肤操作
     *
     * @param decorView
     */
    private void applyDayNightForView(View decorView) {
        if (decorView instanceof ViewsMatch) {
            ViewsMatch viewsMatch = (ViewsMatch) decorView;
            viewsMatch.skinnableView();
        }

        if (decorView instanceof ViewGroup) {
            ViewGroup parent = (ViewGroup) decorView;
            int chidCount = parent.getChildCount();
            for (int i = 0; i < chidCount; i++) {
                applyDayNightForView(parent.getChildAt(i));
            }
        }
    }

}

遍历一遍所有的view即可换肤了。

 

下面是测试的代码:

package com.example.skinproject;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;

import com.example.skinlibrary.SkinActivity;

import java.io.File;

public class SkinTestActivity extends SkinActivity {

    private String skinPath;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_skin_test);

        skinPath = Environment.getExternalStorageDirectory() + File.separator + "net163.skin";
        Log.d("SkinTestActivity", "路径=" + skinPath);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
            if (checkSelfPermission(perms[0]) == PackageManager.PERMISSION_DENIED) {
                requestPermissions(perms, 200);
            }
        }
    }

    @Override
    public boolean useSkin() {
        return true;
    }

    //还原
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public void def(View view) {
        Log.d("SkinTestActivity", "time>>>>>>>>>");
        long start = System.currentTimeMillis();
        defaultSkin(R.color.colorPrimary);

        Log.d("SkinTestActivity", "还原耗时= time>>>>>>>>>" + (System.currentTimeMillis() - start));
    }

    //换肤
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public void skin(View view) {
        Log.d("SkinTestActivity", "time>>>>>>>>>");
        long start = System.currentTimeMillis();
        //第二个参数的主题色
        skinDynamic(skinPath, R.color.skin_item_color);
        Log.d("SkinTestActivity", "time>>>>>>>>>");
        Log.d("SkinTestActivity", "换肤耗时= time>>>>>>>>>" + (System.currentTimeMillis() - start));
    }
}

6.0需要申请权限。因为皮肤包是放在apk外部的。

源码路径:https://download.csdn.net/download/qq_40207976/16660733

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值