不只是切换多语言Android(一)

前言:前几天在地铁上看到一哥们的一篇app换肤方案解决,觉得很nice,于是就研究了一下,收获蛮多,还想到以前我做过的app,当时是需要中英文切换的,于是打算把这哥们的换肤方案运用在切换语言上去,捣腾了一会,感觉还可以,但是自我感觉还是不太理想,不管咋样,就当涨涨见识呗,于是打算把我所学到的知识分享给大家,大家有什么好的切换语言方案记得告知一下哈,先拜谢啦!!

先附上大牛的换肤demo地址:
https://github.com/ximsfei/Android-skin-support

先看一下换肤效果:

这里写图片描述

然后根据换肤思路,看看我们实现的切换语言框架效果:

这里写图片描述

其中主要的思路还是来自v7的AppCompatActivity,AppCompatActivity做的一件事就是把我们的基本组件转换成了material效果组件,我们简单的看一下AppCompatActivity是怎么处理的。

public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
    。。。。
     @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
    }
    。。。。
}

首先创建了一个AppCompatDelegate对象delegate:

 /**
     * @return The {@link AppCompatDelegate} being used by this Activity.
     */
    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }

然后我们看看它是怎么创建mDelegate对象的:

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (BuildCompat.isAtLeastN()) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

找到了delegate对象,我们反过来看看AppCompatActivity最初的oncreate方法:

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);

我们找到installViewFactory方法:

@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                    instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

可以看到在delegate的内部,获取到当前app的 layoutInflater对象,然后给layoutInflater设置了一个factory:

  LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } 

LayoutInflater我们都不陌生,当我们去加载一个xml布局的时候用到:

inflater.inflate(xxxx)

这个方法:

我们顺着这个方法往下走:

        getLayoutInflater().inflate()
ublic View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
。。省略很多代码
                            rInflate(parser, root, inflaterContext, attrs, false);
。。省略很多代码
}

我们看看rInflate方法:

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

我们继续看到其中的createViewFromTag方法:

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
            。。。。
     if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
            。。。。
    }

这个mPrivateFactory就是我们在AppCompatActivity的onCreate方法中调用的:

        delegate.installViewFactory();
@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                    instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

所以这个factory即为AppCompatActivity中的delegate,所以当我们调用setContentView的时候,其中在PhoneWindow中会去获取app的inflator对象,然后调用inflate方法,把通过我们传递的layoutid加载layout文件,在inflater方法中,当inflator设置了factory的话就会执行:

 if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

所以会触发factory的onCreateView方法,把需要创建view的父类、名字、attrs属性传递过去:

比如下面的xml中view,就会传递name=TextView、attrs为text等属性。

<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/change_succ"
        />

我们看看AppCompatActivity中的delegate对象(factory)怎么处理的:

@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);
    }
@Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        final boolean isPre21 = Build.VERSION.SDK_INT < 21;

        if (mAppCompatViewInflater == null) {
            mAppCompatViewInflater = new AppCompatViewInflater();
        }

        // We only want the View to inherit its context if we're running pre-v21
        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                isPre21, /* 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 */
        );
    }

最后看看
mAppCompatViewInflater.createView:

         @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 context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = 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
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        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 it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

好吧,看到这里是不是有点兴奋呢??我们看到,当我们在xml布局中创建一些基本的组件的时候,都会AppCompatActivity转换成了AppCompatTextView等等这类的组件了,好累啊~AppCompatTextView我就不看了,AppCompatTextView等就是实现material属性的关键组件。

好啦~~也不知道小伙伴看懂了没有,简单总结一下:

比如我们在xml中创建一个TextView—>我们调用activity的setcontentview—>inflater的inflate方法--->在AppCompatActivity方法中给inflater设置factory--->调用inflater的factory的oncreateview方法—>然后在oncreateview方法中通过传递的组件name创建对应的组件AppCompatTextView-->根据activity的主题修改AppCompatTextView样式。

换肤方案的实现:

跟AppCompatActivity一样,给inflater设置一个factory--->>调用inflater的factory的oncreateview方法--->然后在oncreateview方法中通过传递的组件name创建对应的组件SkinAbleTextView--->然后把创建的SkinAbleTextView组件用一个集合装起来—>最后通过观察者模式,当变皮肤的时候,遍历所有的组件,然后调用对应的换肤方法--->改变样式

我们的切换语言框架也是一样哈~~~我就不啰嗦了,下节直接撸代码了~

不只是切换多语言Android(二)

本节先到这里!!
欢迎小伙伴入群: qq群号:511276976

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值