Android技能树 — LayoutInflater Factory小结

栏目: Android · 发布时间: 7年前

内容简介:所以我们本文主要学习:估计很多人都会使用AS的Tools — Layout Inspector功能来查看自己写的界面结构及控件的相应元素。比如我们写了很简单的例子:

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

所以我们本文主要学习:

  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查看:

Android技能树 — LayoutInflater Factory小结

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

Android技能树 — LayoutInflater Factory小结

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

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

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

复制代码

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

Android技能树 — LayoutInflater Factory小结

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

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

Android技能树 — LayoutInflater Factory小结

正文

1.LayoutInflater相关知识

其实大部分人使用 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的源码在第三篇)

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

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

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

Android技能树 — LayoutInflater Factory小结

2. Factory相关知识

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

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

protected void onCreate(@Nullable Bundle savedInstanceState) {
    //"1.获取代理类对象"
    AppCompatDelegate delegate = this.getDelegate();
    
    //"2.调用代理类的installViewFactory方法"
    delegate.installViewFactory();
    
    ......
    ......
    super.onCreate(savedInstanceState);
}
复制代码

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

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

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

    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以后引入的:

Android技能树 — LayoutInflater Factory小结

所以我们就直接可以简单理解为 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方法

所以我们这边要记住其实就是调用AppCompatDelegateImpl的createView方法

所以我们这边要记住其实就是调用AppCompatDelegateImpl的createView方法

重要的事情说三遍,因为后面会用到这块

我们继续来分析源码,我们跟踪到 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 方法:

@NonNull
protected AppCompatButton createButton(Context context, AttributeSet attrs) {
    return new AppCompatButton(context, attrs);
}
复制代码

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

2.2 自己实现自定义Factory2

我们现在来具体看下 Factory2onCreateView 方法,我们自己来实现一个自定义的 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);
}

复制代码

我们可以看到 Factory2onCreateView 方法里面的属性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;
    }

});
复制代码

我们可以看下效果:

Android技能树 — LayoutInflater Factory小结

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

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

Android技能树 — LayoutInflater Factory小结

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

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

所以我们可以修改相应的控件的参数,最后再把修改过的内容重新还给 AppCompatDelegateImplcreateView 方法去生成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);
}

复制代码

我们最终可以看到:

Android技能树 — LayoutInflater Factory小结

按钮也的确都变成了AppCompatButton。

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

Android技能树 — LayoutInflater Factory小结

3. 实际项目中的用处

其实以前在一些文章中也看到过,说什么突然你想全局要替换 ButtonTextView ,这样更方便什么的,但是单纯这种直接整个控件替换我个人更喜欢去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动态换肤原理解析及实践

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

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

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

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

Android技能树 — LayoutInflater Factory小结

初步一看是不是感觉很神奇?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;
        
        ......
        ......
    }
复制代码

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

Android技能树 — LayoutInflater Factory小结

以上所述就是小编给大家介绍的《Android技能树 — LayoutInflater Factory小结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mathematica Cookbook

Mathematica Cookbook

Sal Mangano / O'Reilly Media / 2009 / GBP 51.99

As the leading software application for symbolic mathematics, Mathematica is standard in many environments that rely on math, such as science, engineering, financial analysis, software development, an......一起来看看 《Mathematica Cookbook》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具