查看原文
其他

换肤、全局字体替换、无需编写shape、selector 的原理Factory小结

青蛙要fly 鸿洋 2019-04-05

本文作者


作者:青蛙要fly

链接:

https://www.jianshu.com/p/8d8ada21ab82

本文由作者授权发布。


1前言


很久没写文章了,所以打算水一篇文章,毕竟这方面知识的文章有很多很多。


前段时间流行起来了突然不愿意写Shape,Selector文件的文章,然后各种方案,编写自定义View等。那时候大家应该都看到了一篇:

无需自定义View,彻底解放shape,selector吧


我发现这个想法挺好的,所以今天就一步步来讲解下跟这个方案有关的相关基础知识点,看完后大家基本就会懂了,然后可以自己编写。


所以我们本文主要学习:


1. LayoutInflater相关知识(⭐️科普为主)

2. setFactory相关知识(⭐️⭐️⭐️本文主要知识点)

3. 实际项目中的用处(⭐️⭐️科普为主️)


估计很多人都会使用AS的Tools — Layout Inspector功能来查看自己写的界面结构及控件的相应元素。


比如我们写了很简单的例子:


public class TestActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
    }
}


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">



    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"
        />


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="textview"
        />


    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        />


</LinearLayout>


然后用AS查看:



大家有没有看到有没有什么特别的地方:



我们在布局中写的是Button,TextView,ImageView,但是在AS的Layout Inspector功能查看下,变成了AppCompatButton,AppCompatTextView,AppComaptImageView,那到底是我们的按钮真的已经在编译的时候自动变成了AppCompatXXX系列,还是只是单纯的在这个工具里面看的时候我们的控件只是显示给我们看到的名字是AppCompatXXX系列而已。


我们把我们的Activity的父类做下修改,改为:


public class TestActivity extends AppCompatActivity{
    ......
}
变为
public class TestActivity extends Activity{
    ......
}


我们再来查看下Layout Inspector界面:



我们可以看到,控件就自动变成了我们布局里面写的控件名称了, 那就说明,我们继承的AppCompatActivity对我们xml里面写的控件做了替换。


而AppCompatActivity的替换主要是通过LayoutInflater setFactory


2LayoutInflater相关知识


其实大部分人使用LayoutInflater的话,更多的是使用了inflate方法,用来对Layout文件变成View:


View view = LayoutInflater.from(this).inflate(R.layout.activity_test,null);


甚至于我们平常在Activity里面经常写的setContentView(R.layout.xxx);方法的内部也是通过inflate方法实现的。


有没有想过为什么调用了这个方法后,我们就可以拿到了相关的View对象了呢?


其实很简单,就是我们传入的是一个xml文件,里面通过xml格式写了我们的布局,而这个方法会帮我们去解析XML的格式,然后帮我们实例化具体的View对象即可,我们具体一步步来看源码:


public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}


public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }

    //"可以看到主要分为2步"
    //"第一步:通过res.getLayout方法拿到XmlResourceParser对象"
    final XmlResourceParser parser = res.getLayout(resource);

    try {

        //"第二步:通过inflate方法最终把XmlResourceParser转为View实例对象"
        return inflate(parser, root, attachToRoot);

    } finally {
        parser.close();
    }
}


本来我想大片的源码拷贝上来,然后一步步写上内容,但是后来发现一个讲解资源获取过程的不错的系列文章,所以我就直接借鉴大佬的,直接贴上链接了:


(关于本文的内容相关的,可以着重看下第一篇和第三篇,inflate的源码在第三篇)


  1. Android资源管理框架(Asset Manager)(一)简介

    https://blog.csdn.net/kc58236582/article/details/53186104

  2. Android资源管理框架(二)AssetManager创建过程

    https://blog.csdn.net/kc58236582/article/details/53187485

  3. Android资源管理框架(三)应用程序资源的查找过程

    https://blog.csdn.net/kc58236582/article/details/53196071


3Factory相关知识


3.1 源码中默认设置的Factory2相关代码


我们在前言中的例子中可以看到我们的Activity继承了AppCompatActivity,我们来查看AppCompatActivity的onCreate方法:


protected void onCreate(@Nullable Bundle savedInstanceState) {
    //"1.获取代理类对象"
    AppCompatDelegate delegate = this.getDelegate();

    //"2.调用代理类的installViewFactory方法"
    delegate.installViewFactory();

    ......
    ......
    super.onCreate(savedInstanceState);
}


我们可以看到和Activity的onCreate方法最大的不同就是AppCompatActivity把onCreate种的操作都放在了代理类AppCompatDelegate中的onCreate方法中处理了,而AppCompatDelegate是抽象类,具体的实现类是AppCompatDelegateImpl,


//"1.获取代理类具体方法源码:"

@NonNull
public AppCompatDelegate getDelegate() {
    if (this.mDelegate == null) {
        this.mDelegate = AppCompatDelegate.create(thisthis);
    }

    return this.mDelegate;
}

public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}


我们再来看代理类的installViewFactory方法具体实现:


public void installViewFactory() {

    //'获取了LayoutInflater对象'
    LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);


    if (layoutInflater.getFactory() == null) {
        //'如果layoutInflater的factory2为null,对LayoutInflater对象设置factory'
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
        Log.i("AppCompatDelegate""The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
    }
}


AppCompatDelegateImpl自己实现了Fatory2接口,所以就直接setFactory2(xx,this)即可,我们来看下Factory2到底是啥:


public interface Factory2 extends Factory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}


可能很多人在以前看过相关文章,都是Factory接口及方法是setFactory,对于Factory2是一脸懵逼,我们可以看到上面的Factory2代码,Factory2其实就是继承了Factory接口,其实setFactory方法已经被弃用了,而且你调用setFactory方法,内部其实还是调用了setFactory2方法,setFactory2是在SDK>=11以后引入的:



所以我们就直接可以简单理解为Factory2类和setFactory2方法是用来替代Factory类和setFactory方法


所以也就执行了AppCompatDelegateImpl里面的onCreateView方法:


//'调用方法1'
public View onCreateView(String name, Context context, AttributeSet attrs) {
    return this.onCreateView((View)null, name, context, attrs);
}

//'调用方法2'
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    this.createView(parent, name, context, attrs);
}

//'调用方法3'
public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {

    //先实例化mAppCpatViewInflater对象代码
    ......
    ......

    //'直接看这里,最后调用了mAppCompatViewInflater.createView方法返回相应的View'
    return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}


所以通过上面我们可以看到,最终设置的Factory2之后调用的onCreateView方法,其实就是调用AppCompatDelegateImpl的createView方法(最终调用了AppCompatViewInflater类中的createView方法).


所以我们这边要记住其实就是调用AppCompatDelegateImpl的createView方法</br>
所以我们这边要记住其实就是调用AppCompatDelegateImpl的createView方法</br>
所以我们这边要记住其实就是调用AppCompatDelegateImpl的createView方法</br>
重要的事情说三遍,因为后面会用到这块


我们继续来分析源码,我们跟踪到AppCompatViewInflater类中的createView方法(这里以Button为例,其他的代码暂时去除):


final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {

    ......
    ......

    View view = null;
    byte var12 = -1;
    switch(name.hashCode()) {

    ......
    ......

    case 2001146706:
        if (name.equals("Button")) {
            var12 = 2;
        }
    }

    switch(var12) {

    ......
    ......

    case 2:
        view = this.createButton(context, attrs);
        this.verifyNotNull((View)view, name);
        break;

    ......
    ......

    return (View)view;
}


我们来看createButton方法:


@NonNullprotected AppCompatButton createButton(Context context, AttributeSet attrs) {    return new AppCompatButton(context, attrs); }

所以我们看到了,最终我们的Button替换成了AppCompatButton。


3.2 自己实现自定义Factory2


我们现在来具体看下Factory2的onCreateView方法,我们自己来实现一个自定义的Factory2类,而不是用系统自己设置的:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {

    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        //'这个方法是Factory接口里面的,因为Factory2是继承Factory的'
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }

        //'这个方法是Factory2里面定义的方法'
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            Log.e(TAG, "parent:" + parent + ",name = " + name);
            int n = attrs.getAttributeCount();
            for (int i = 0; i < n; i++) {
                Log.e(TAG, attrs.getAttributeName(i) + " , " + attrs.getAttributeValue(i));
            }
            return null;
        }

    });

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test1);
}


我们可以看到Factory2的onCreateView方法里面的属性parent指的是父View对象,name是当前这个View的xml里面的名字,attrs 包含了View的属性名字及属性值。


打印后我们可以看到打印出来了我们的demo中的Layout布局中写的三个控件了。


......
......
......

E: parent:android.widget.LinearLayout{4e37f38 V.E...},name = Button
E: layout_width , -2
E: layout_height , -2
E: text , button
E: parent:android.widget.LinearLayout{4e37f38 V.E...},name = TextView
E: layout_width , -2
E: layout_height , -2
E: text , textview
E: parent:android.widget.LinearLayout{4e37f38 V.E...},name = ImageView
E: layout_width , -2
E: layout_height , -2
E: src , @2131361792


正好的确是我们layout中设置的控件的值。我们知道了在这个onCreateView方法中,我们可以拿到当前View的内容,我们学着系统替换AppCompatXXX控件的方式更换我们demo中的控件,加上这段代码:


......
......
......
LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) 
{
        return null;
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) 
{

        //'我们在这里对传递过来的View做了替换'
        //'把TextView和ImageView 都换成了Button'
        if(name.equals("TextView") || name.equals("ImageView")){
            Button button = new Button(context, attrs);
            return button;
        }

        return null;
    }

});


我们可以看下效果:



我们知道了在onCreateView中,可以看到遍历的所有View的名字及属性参数,也可以在这里把return的值更改做替换。


但是我们知道系统替换了的AppCompatXXX控件做了很多兼容,如果我们像上面一样把TextView和ImageView直接换成了Button,那么系统也因为我们设置过了Factory2,就不会再去设置了,也就不会帮我们自动变成AppCompatButton,而是变成了三个Button。



所以我们不能单纯盲目的直接使用我们的Factory2,所以我们还是用的系统最终构建View的方法,只不过在它构建前,更改参数而已,这样最终还是会跑系统的代码。


我们前面代码提过<font color = "red">最终设置的Factory2之后调用的onCreateView方法,其实就是调用AppCompatDelegateImpl的createView方法</font>(就是前面讲的,重要的事情说三遍那个地方,忘记的可以回头再看下)


所以我们可以修改相应的控件的参数,最后再把修改过的内容重新还给AppCompatDelegateImpl的createView方法去生成View即可,这样系统原本帮我们做的兼容性也都还在。


所以我们这里要修改代码为:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) 
{

    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        //'这个方法是Factory接口里面的,因为Factory2是继承Factory的'
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) 
{
            return null;
        }

        //'这个方法是Factory2里面定义的方法'
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) 
{
            if(name.equals("TextView") || name.equals("ImageView")){
                name = "Button";
            }

            //'我们只是更换了参数,但最终实例化View的逻辑还是交给了AppCompatDelegateImpl'
            AppCompatDelegate delegate = getDelegate();
            View view = delegate.createView(parent, name, context, attrs);
            return view;
        }
    });

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test1);
}


我们最终可以看到:



按钮也的确都变成了AppCompatButton。


总结:设置Factory2更像是在系统填充View之前,先跑了一下onCreateView方法,然后我们可以在这个方法里面,在View被填充前,对它进行修改。


4实际项目中的用处


其实以前在一些文章中也看到过,说什么突然你想全局要替换Button到TextView,这样更方便什么的,但是单纯这种直接整个控件替换我个人更喜欢去xml文件里面改,因为一般一个app是团队一起开发,然后你这么处理,后期别人维护时候,看了xml,反而很诧异,后期维护我个人感觉不方便。


所以我这个列举了几个常用的功能:


3.1. 全局替换字体等属性


因为字体等是TextView的一个属性,为了加一个属性,我们就没必要去全部的布局中进行更改,只需要上我们的onCreateView中,发现是TextView,就去设置我们对应的字体。


public static Typeface typeface;
@Override
protected void onCreate(Bundle savedInstanceState)
{
    if (typeface == null){
        typeface = Typeface.createFromAsset(getAssets(), "xxxx.ttf");
    }
    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }

        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            AppCompatDelegate delegate = getDelegate();
            View view = delegate.createView(parent, name, context, attrs);

            if ( view!= null && (view instanceof TextView)){
                ((TextView) view).setTypeface(typeface);
            }
            return view;
        }

    });

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}


3.2 动态换肤功能


这块动态换肤功能,网上的文章也很多,但是基本的原理都一样,也是用了我们本文的知识,和上面的更换字体类似,我们可以对做了标记的View进行识别,然后在onCreateView遍历到它的时候,更改它的一些属性,比如背景色等,然后再交给系统去生成View。


具体可以参考下:Android动态换肤原理解析及实践

https://www.jianshu.com/p/c2b1ae56381e


3.3 无需编写shape、selector,直接在xml设置值


估计前端时间大家在掘金都看到过这篇文章:


无需自定义View,彻底解放shape,selector吧

https://juejin.im/post/5b9682ebe51d450e543e3495


里面讲到我们如果要设置控件的角度等属性值,不需要再去写特定的shape或者selector文件,直接在xml中写入:



初步一看是不是感觉很神奇?what amazing !!


其实核心也是使用了我们今天讲到的知识点,自定义Factory类,只需要在onCreateView方法里面,判断attrs的参数名字,比如发现名字是我们制定的stroke_color属性,就去通过代码手动帮他去设置这个值,我们来查看下它的部分代码,我们直接看onCreateView方法即可:


@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {

    ......
    ......

    if (typedArray.getBoolean(R.styleable.background_ripple_enable, false) &&
        typedArray.hasValue(R.styleable.background_ripple_color)) {
        int color = typedArray.getColor(R.styleable.background_ripple_color, 0);
            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                Drawable contentDrawable = (stateListDrawable == null ? drawable : stateListDrawable);
                RippleDrawable rippleDrawable = new RippleDrawable(ColorStateList.valueOf(color), contentDrawable, contentDrawable);
                view.setClickable(true);
                view.setBackground(rippleDrawable);
            } else {
                StateListDrawable tmpDrawable = new StateListDrawable();
                GradientDrawable unPressDrawable = DrawableFactory.getDrawable(typedArray);
                unPressDrawable.setColor(color);
                tmpDrawable.addState(new int[]{-android.R.attr.state_pressed}, drawable);
                tmpDrawable.addState(new int[]{android.R.attr.state_pressed}, unPressDrawable);
                view.setClickable(true);
                view.setBackground(tmpDrawable);
            }
        }
        return view;

        ......
        ......
    }


是不是这么看,大家基本就懂了原理,这样你再去看它的库,或者要加上什么自己特定的属性,都有能力自己去进行修改了。


3.4 XXXXX


当然还有很多奇思妙想的用处,只要大家想象力够多,就可以在这中间做各种骚操作。


不小心把文章就水完了.......有错误欢迎大家指出。


推荐阅读

这效果炸了  图片粒子“爆炸”效果

分享一下三年来公众号的小经验

Toast 不显示了?


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存