Android静态换肤实现

静态换肤是在APP应用内部放置多套资源,进行资源的切换,静态换肤一般用在APP的日夜间模式切换中。
一.原理
当在Activity中使用setContentView加载布局时,会调用到AppCompatDelegateImpl的setContentView方法,该方法代码如下:
	@Override
    public void setContentView(int resId) {
        ...
            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;
        ...
    }
以上代码中,通过tryCreateView方法进行控件的创建,tryCreateView的代码如下:
public final View tryCreateView(@Nullable View parent, @NonNull String name,
    ...
        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;
    }
在tryCreateView方法中,首先通过mFactory2的onCreateView方法创建View对象,mFactory2的类型是Factory2,Factory2是一个接口,只有一个onCreateView方法,Activity就实现了这个接口。mFactory2初始化过程在AppComPatActivity类的onCreate方法中,通过delegate.installViewFactory()方法进行初始化,该方法如下:
@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");
            }
        }
    }
只有当layoutInflater.getFactory()为空时,会采用AppCompatDelegateImpl作为Factory2对象,然后后续执行AppCompatDelegateImpl的createView方法创建View对象。
基于以上流程,静态换肤可以采取以下方式:
  • 在代码执行delegate.installViewFactory()方法之前,将Factory2对象设置成我们自己写的Factory2,这样通过createView进行View对象的创建可以按照我们的思路进行。只需在super.onCreate方法执行之前调用LayoutInflaterCompat.setFactory2方法设置我们自己的Factory2对象。
  • 在创建View对象时,拿到View的background、textColor等与换肤相关的属性,在换肤时通过改变属性值进行换肤。
二.静态换肤实现:
1.SkinActivity
首先新建一个SkinActivity作为App中一切Activity的基类,SkinActivity继承自AppCompatActivity。其onCreate方法如下:
@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    //在super.onCreate方法执行之前,把Factory2对象设置为SkinActivity对象,用来执行自定义的onCreateView过程。
       	LayoutInflaterCompat.setFactory2(LayoutInflater.from(this),this);
        super.onCreate(savedInstanceState);
    }
由于Activity都实现了Factory2接口,所以可以重写SkinActivity的onCreateView方法,该方法如下:
@Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        if (openSkin()){
            //如果允许换肤,则加载自定义的View去代替Android原生View
            if (null == viewInflater){
                viewInflater = new MyAppCompatViewInflater(context);
            }
            viewInflater.setName(name);
            viewInflater.setAttrs(attrs);

            return viewInflater.createView();
        }
        return super.onCreateView(name, context, attrs);
    }
    
    public View createView(){
        View resultView = null;
        switch (name){
            //根据xml布局中的控件名称寻找自定义的控件进行替换
            case "TextView":
                resultView = new MyTextView(context,attrs);
                break;
            case "ImageView":
                resultView = new MyImageView(context,attrs);
                break;
            case "Button":
                resultView = new MyButton(context,attrs);
                break;
            case "LinearLayout":
                resultView = new MyLinearLayout(context,attrs);
                break;
            case "RelativeLayout":
                resultView = new MyRelativeLayout(context,attrs);
                break;
        }

        return resultView;
    }
2.自定义View
在布局加载时,需要使用自定义的View去代替Android中的View,在自定义View之前,首先新建一个ViewChange接口,该接口包含一个换肤方法skinChange,我们自定义的View需要实现该接口用来执行换肤操作。
以自定义Button为例,首先在/res/valuse/attrs.xml下把Button控件换肤时需要变换的属性列出来,如下所示:
 <resources>
    //换肤时对背景、文字颜色、文字内容作变换
        <declare-styleable name="MyButton">
            <attr name="android:background"/>
            <attr name="android:textColor"/>
            <attr name="android:text"/>
        </declare-styleable>

    </resources>
新建一个类MyButton,该类继承Button并且实现ViewChange接口。在MyButton的构造方法中,对换肤时需要变化的三个属性及其对应的ResourceId使用sparseIntArray进行保存。
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        attrsBean = new AttrsBean();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyButton,defStyleAttr,0); attrsBean.saveViewResource(typedArray,R.styleable.MyButton);
        typedArray.recycle();
    }
    
    public void saveViewResource(TypedArray typedArray, int[] styleable){
        for (int i=0;i < typedArray.length();i++){
            int key = styleable[i];
            int resourceId = typedArray.getResourceId(i,DEFAULT_VALUE);
            sparseIntArray.put(key,resourceId);
        }
    }
在重写的ViewChange接口的换肤方法skinChange中,拿到保存在sparseIntArray中的属性id和相应的ResourceId,通过ResouceId得到资源并给控件属性设置值。
@Override
    public void skinChange() {
        //得到attrs中的属性值
        int key = R.styleable.MyButton[R.styleable.MyButton_android_background];
        //得到属性值对应的ResourceId
        int backgroundId = attrsBean.getViewResource(key);
        if (backgroundId > 0){
            //通过ResourceId得到资源对象
            Drawable drawable = ContextCompat.getDrawable(getContext(), backgroundId);
            //为控件属性设置背景值
            setBackgroundDrawable(drawable);
        }

        key = R.styleable.MyButton[R.styleable.MyButton_android_textColor];
        int textColorId = attrsBean.getViewResource(key);
        if (textColorId > 0){
            ColorStateList colorStateList = ContextCompat.getColorStateList(getContext(), textColorId);
            setTextColor(colorStateList);
        }
        key = R.styleable.MyButton[R.styleable.MyButton_android_text];
        int textId = attrsBean.getViewResource(key);
        if (textId > 0){
            String str = getContext().getString(textId);
            setText(str);
        }
    }
3.App中的Activity
在写App中的xml布局时,需要准备日间和夜间模式两套资源,夜间模式的资源放在drawable-night和values-night中,Android在Configuration类中提供了两个常量UI_MODE_NIGHT_NO和UI_MODE_NIGHT_YES表示日间和夜间模式,当处于夜间模式时,布局会加载drawable-night和values-night中的资源。
对于App中的Activity,需要继承SkinActivity,当进行换肤操作时,执行如下方法:
public void dayOrNight(View view) {
        //获得当前模式:
        // Configuration.UI_MODE_NIGHT_NO为日间模式
        //Configuration.UI_MODE_NIGHT_YES为夜间模式
        int uiMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
        switch (uiMode){
            case Configuration.UI_MODE_NIGHT_NO:
                //如果是当前模式是日间模式,则该为夜间模式
                setDayNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                break;
            case Configuration.UI_MODE_NIGHT_YES:
                setDayNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                break;
        }
    }
其中setDayNightMode方法写在SkinActivity中。
public void setDayNightMode(int mode){
        //设置当前模式值
        getDelegate().setLocalNightMode(mode);
        View decorView = getWindow().getDecorView();
        changeSkin(decorView);
    }
    
    private void changeSkin(View decorView){
        //从顶层decorView开始,如果View支持换肤,则进行换肤操作。
        if (decorView instanceof ViewChange){
            ViewChange viewChange = (ViewChange)decorView;
            viewChange.skinChange();
        }
        if (decorView instanceof ViewGroup){
            ViewGroup viewGroup = (ViewGroup)decorView;
            for(int i=0;i<viewGroup.getChildCount();i++){
                changeSkin(viewGroup.getChildAt(i));
            }
        }
    }
换肤效果如图所示:

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值