Android 中LayoutInflater(布局加载器)之实战篇

1,351 阅读11分钟
原文链接: blog.csdn.net

本文出自博客Vander丶CSDN博客,如需转载请标明出处,尊重原创谢谢
博客地址:blog.csdn.net/l540675759/…

前言

如果读者没有阅读过该系列博客,建议先阅读下博文说明,这样会对后续的阅读博客思路上会有一个清晰的认识。

Android 中LayoutInflater(布局加载器)系列博文说明


导航

Android 中LayoutInflater(布局加载器)系列博文说明

Android 中LayoutInflater(布局加载器)系列之介绍篇

Android 中LayoutInflater(布局加载器)系列之源码篇

Android 中LayoutInflater(布局加载器)源码篇之createViewFromTag方法

Android 中LayoutInflater(布局加载器)源码篇之rInflate方法

Android中LayoutInflater(布局加载器)系列之实战篇


效果

小红书动画

可以看出在滑动时,会出现视觉差效果。

物体散出

可以看出在滑动时,物品会飘出去。


概述

(1)主要目的是通过这个Demo,理解自定义LayoutInflater.Factory的过程。

(2)理解小红书的第一版引导页是如何制作出来的。


分析

这个效果属于视觉差的效果,原理是根据ViewPager的滑动方向,页面内物理做同向偏移,只要偏移距离大于页面的偏移,就会产生速度差,那么就会实现该效果。

实现速度差,我们需要一个滑动的比例系数:

在页面进入时:

页面物体的移动距离 = (页面长度 - 滑动距离) * 滑动系数

在页面滑出时:

页面物体的移动距离 = (0 - 滑动距离 ) * 滑动系数

同时考虑第二张Gif上,发现物体Y轴也存在移动,所以也得需要考虑Y轴方向的滑动,整理下:

//进入时:
view.setTranslateX((vpWidth - positionOffsetPixels) * xIn);
view.setTranslateY((vpWidth - positionOffsetPixels) * yIn);

//退出时
view.setTranslateX((0 - positionOffsetPixels) * xOut);
view.setTranslateY((0 - positionOffsetPixels) * yOut);

这样就可以实现出:

(1)进入该界面时,界面上的物品快速飞进来。

(2)退出该界面时,界面上的物理快速飞出去。


实现思路

对于上述的分析,这里的实现思路存在两种:

  1. 自定义View,自定义xIn、yIn、xOut、yOut四个属性的系数,所有界面上的物体继承这个自定义View。

  2. 自定义LayoutInflater.Factory在解析时,将这些自定义属性提取,以Tag方式储存起来。


优缺点分析

自定义View:

优点:可以对物体做更多层面的扩展,这个自定义LayoutInflater.Factory是不具备的。

缺点:由于界面的物体数量过多,在findViewById时需要处理的View元素过多,极大的增加代码量。

自定义LayoutInflater.Factory :

优点:可以在解析过程中对View做统一操作,当出现大量的View时,能够缩减大量代码。

缺点:在解析时预处理View,但是就不能动态的改变View的属性,要对View进行扩展性操作,自定义LayoutInflater.Factory不具备这样的功能。


自定义LayoutInflater.Factory

上述的两种方案的优缺点已经分析完毕,但是本文作为实战篇,所以只会介绍自定义LayoutInflater.Factory这种方式。

在实际场景中,需要结合自身情况,以及上述的优缺点,进行合理选择。

在介绍之前,先看一段代码:

            View view;
            //如果Factory2存在,就会调用其onCreateView方法
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
                //如果Factory存在,就会调用其onCreateView方法,和Factory2不同的时,这里的参数没有父View
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            //如果没有Factory或者Factory2,就会寻找mPrivateFactory(本质上也是Factory2)
            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

这段代码出自LayoutInflater中createViewFromTag()方法,作用是根据View的名称(name参数)来创建View,这里在源码篇已经详细分析过,如果没有看过,可以点击这里。

Android 中LayoutInflater(布局加载器)源码篇之createViewFromTag方法

在这里就简单描述下,这个方法的主要流程:

  1. 对一些特殊标签,做分别处理,例如:view,TAG_1995(blink)

  2. 进行对Factory、Factory2的设置判断,如果设置那么就会通过设置Factory、Factory2进行生成View

  3. 如果没有设置Factory或Factory2,那么就会使用LayoutInflater默认的生成方式,进行View的生成

在实战篇中,只有第二部分和我们今天的内容是相关的,我们在看一遍第二条。

进行对Factory、Factory2的设置判断,如果设置那么就会通过设置Factory、Factory2进行生成View

如果设置了Factory或者Factory2,那么就不会使用LayoutInflater默认的生成方式,那么生成View的过程,就由我们自主把控,这才是我们自定义LayoutInflater.Factory的主要原因。


自定义Factory还是Factory2 ?

            View view;
            //如果Factory2存在,就会调用其onCreateView方法
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
                //如果Factory存在,就会调用其onCreateView方法,和Factory2不同的时,这里的参数没有父View
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

我们能够从这段代码中得出,Factory2比Factory的优先级要高,即Factory2存在Factory就不可能会被调用,同理可以得出结论:

优先级顺序:

mFactory2  > mFactory > mPrivateFactory > LayoutInflater默认处理方式

而且我们还能够发现mFactory2的onCreateView()方法与mFactory是不相同的:

//mFactory2
mFactory2.onCreateView(parent, name, context, attrs);

//mFactory
view = mFactory.onCreateView(name, context, attrs);

根据上述的分析,我们可以得出结论:

(1)Factory2的调用优先级比Factory要高

(2)Factory2的onCreateView()方法,会比Factory多返回一个父View的参数。

(3)Factory2和Factory是互斥的,(如果不通过反射的话)只能设置一个。

第三条在CreateViewFromTag的那篇文章已经分析过了,这里不做过多的解释了。

实际选择的过程中,一般会选择自定义Factory2,因为Factory2本身也继承了Factory接口,而且Factory2的优先级比较高。


注意事项

(1)设置Factory但是发现无响应,是因为本身LayoutInflater中存在Factory2

因为一般使用方式,是直接调用cloneInContext()方法,我们知道一般的默认解析器都是PhoneLayoutInflater,我们看下其实现方式:

    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }

本质就是调用LayoutInflater的两参构造方法:

    protected LayoutInflater(LayoutInflater original, Context newContext) {
        mContext = newContext;
        mFactory = original.mFactory;
        mFactory2 = original.mFactory2;
        mPrivateFactory = original.mPrivateFactory;
        setFilter(original.mFilter);
    }

在这里可以看出,cloneInContext会把原LayoutInflater的Factory2和Factory一并复制。

因为Factory比Factory2的优先级低,所以才会不出现效果。

解决方案 :

(1)自定义LayoutInflater,并且改写cloneInContext,使其不复制原LayoutInflater的Factory2以及Factory。

public class CustomLayoutInflater extends LayoutInflater {

    protected CustomLayoutInflater(Context context) {
        super(context);
    }

    @Override
    public LayoutInflater cloneInContext(Context newContext) {
        return new CustomLayoutInflater(newContext);
    }
}

(2)使用时,直接通过new出实例,然后setFactory

       CustomLayoutInflater newInflater = new CustomLayoutInflater(getActivity());
        newInflater.setFactory2(new CustomAppFactory(newInflater, this));
        return newInflater.inflate(layoutId, null);

(2)使用AppCompatActivity直接setFactory2或者setFactory为什么报错?

这是因为 AppCompatActivity 在初始化的时候,已经设置了 Factory,下面来看下这部分代码

  @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        //注意这个方法
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        //.....省略多余的代码..........
        }
        super.onCreate(savedInstanceState);
    }

继续查看 installViewFactory()方法

   @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
        //这句话是设置 Factory 的方法
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            //省略部分代码。。。。。。      
        }
    }

可以发现,在onCreate 时 LayoutInflater 已经设置过一次 Factory 了,然后我再来看下 setFactory() 的源码:

    public void setFactory(Factory factory) {
        if (mFactorySet) {
        //原因就是这一句
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = factory;
        } else {
            mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
        }
    }

根据上面代码,就可以发现报错原因了。

解决方案 :

在使用前,先使用 cloneInContext()克隆出一个新的 LayoutInflater,然后在进行设置操作。

LayoutInflate  newInflater = LayoutInflater.cloneInContext(inflater,context);

newInflater.setFactory(new CustomFactory());

这样就避开在原 LayoutInflater 设置 Factory 报错了。


自定义Factory2的实现 ——> CustomAppFactory

根据上面的展示效果,我们可以判断出是ViewPager + Fragment的风格,所以我们自定义Factory应该在Fragment的onCreateView中,更改LayoutInflater。

而且根据注意事项,我们一般会自定义优先级较高的Factory2,防止本身cloneInContext的LayoutInflater中已经存在Factory2,我们使用Factory会无效。

使用方式:

    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Bundle bundle = getArguments();
        int layoutId = bundle.getInt(LAYOUT_ID);
        //注意需要调用cloneInContext方法生成新的LayoutInflater
        LayoutInflater newInflater = inflater.cloneInContext(getActivity());
        //调用的是setFactory2而非setFactory
        newInflater.setFactory2(new CustomAppFactory(newInflater, this));
        return newInflater.inflate(layoutId, null);
    }

自定义过程

那么就创建一个类CustomAppFactory来实现Factory2的接口,复写onCreateView的方法。

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        View view = null;
        //<<<<<<<<<<<<<<<<<<<<<<<<<<<第一部分>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        try {
            if (name.contains(".")) {
                String checkName = name.substring(name.lastIndexOf("."));
                String prefix = name.substring(0, name.lastIndexOf("."));
                view = defaultInflater(checkName, prefix, attrs);
            }
            if (name.equals("View") || name.equals("ViewGroup")) {
                view = defaultInflater(name, sClassPrefix[1], attrs);
            } else {
                view = defaultInflater(name, sClassPrefix[0], attrs);
            }
            //<<<<<<<<<<<<<<<<<<<<<<<<<<<第二部分>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
            //实例化完成
            if (view != null) {
                //获取自定义属性,通过标签关联到视图上
                setViewTag(view, context, attrs);
                mInflaterView.addView(view);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return view;
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View view = onCreateView(name, context, attrs);
        return view;
    }

其实如果我们采取自定义的方式,这里只会调用onCreateView()四位参数的方法,因为在比较Factory2和Factory的代码也介绍过了。

我们实现的逻辑是在onCreateView()三位逻辑里面,因为需要实现的效果不需要Parent(父View),所以这里逻辑实现全在三位参数的onCreateView()中。

在这里我们将onCreateView()中,分成2部分内容:

(1)根据名称解析出View

(2)扩展操作,将额外的属性,提取出来储存在Tag中


onCreateView第一部分内容

         if (name.contains(".")) {
                String checkName = name.substring(name.lastIndexOf("."));
                String prefix = name.substring(0, name.lastIndexOf("."));
                view = defaultInflater(checkName, prefix, attrs);
            }
            if (name.equals("View") || name.equals("ViewGroup")) {
                view = defaultInflater(name, sClassPrefix[1], attrs);
            } else {
                view = defaultInflater(name, sClassPrefix[0], attrs);
            }

这里判断了name中是否包含“.”,是用来判断生成的View是否是自定义View,下面来看下自定义View和Android自带的组件的区别:

//原生的组件
RelativeLayout
//自定义View
com.demo.guidepagedemo.customview.CustomImageView

可以发现区别为原生的View不带前缀,而自定义View是包括前缀的,所以会用name.contains(“.”)来区分。

而原生组件中View和ViewGroup是属于android.view包下,其他的例如:RelativeLayout,LinearLayout是属于android.widget包下。

    private final String[] sClassPrefix = {
            "android.widget.",
            "android.view."
    };

所以在之后会对View和ViewGroup作区分,上面把sClassPrefix贴出来了。

而这里真正的解析过程最后还是交给LayoutInflater,调用LayoutInflater的onCreateView方法:

    private View defaultInflater(String name, String prefix, AttributeSet attrs) {
        View view = null;
        try {
            view = mInflater.createView(name, prefix, attrs);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return view;
    }

LayoutInflater的onCreateView方法这里就不介绍了,在这里已经分析过了

Android 中LayoutInflater(布局加载器)源码篇之createViewFromTag方法


onCreateView第二部分内容

            //实例化完成
            if (view != null) {
                //获取自定义属性,通过标签关联到视图上
                setViewTag(view, context, attrs);
                mInflaterView.addView(view);
            }

在这里做拓展处理的,setViewTag方法是处理View的自定义属性,然后将这些属性包装成类,给View设置Tag

setViewTag方法

    /**
     * 将View的属性信息存储在Tag中
     */
    private void setViewTag(View view, Context context, AttributeSet attrs) {
        //解析自定义的属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomImageView);
        if (attrs != null && array.length() > 0) {
            AttrTagBean bean = new AttrTagBean();
            bean.xIn = array.getFloat(R.styleable.CustomImageView_in_value_x, 0f);
            bean.xOut = array.getFloat(R.styleable.CustomImageView_out_value_x, 0f);
            bean.yIn = array.getFloat(R.styleable.CustomImageView_in_value_y, 0f);
            bean.yOut = array.getFloat(R.styleable.CustomImageView_out_value_y, 0f);
            //index
            view.setTag(bean);
        }
        array.recycle();
    }

上面对应的是本文我们开始设置的4个系数:

R.styleable.CustomImageView_in_value_x              -->   进入时 x方向的系数

R.styleable.CustomImageView_out_value_x             -->   退出时 x方向的系数

R.styleable.CustomImageView_in_value_y              -->   进入时 y方向的系数

R.styleable.CustomImageView_out_value_y             -->   退出时 y方向的系数

而这里的mInflaterView是一个抽象接口,让Fragment来实现的,通过在Fragment中内置一个List《View》,到时候可以遍历统一操作这些View,下面是实现过程:

public interface InflaterViewImpl {

    /**
     * 获取View集合
     *
     * @return
     */
    List<View> getViews();


    /**
     * 添加元素
     */
    void addView(View view);
}

Fragment中的实现过程:

public class PageFragment extends Fragment implements InflaterViewImpl {

    private List<View> views = new ArrayList<>();

    //**************篇幅原因省略了部分方法************************//

    @Override
    public List<View> getViews() {
        return views;
    }

    @Override
    public void addView(View view) {
        if (views.contains(view)) {
            return;
        }
        views.add(view);
    }
}

处理ViewPager的滑动

这是实战篇的最后一部分内容,主要介绍的是ViewPager的滑动监听相关的处理,因为所有效果是基于ViewPager的滑动监听来显示的。

因为本文主要介绍内容是自定义LayoutInflater.Factory,所以这里会简单叙述下:

 mInflaterVp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                //获取ViewPager的宽度
                int vpWidth = mInflaterVp.getWidth();
                //获取正在进入的界面
                PageFragment inFragment = getPosition(position - 1);
                if (inFragment != null) {
                    List<View> views = inFragment.getViews();
                    if (views != null && views.size() > 0) {
                        for (View view : views) {
                            AttrTagBean tag = (AttrTagBean) view.getTag();
                            if (tag != null) {
                                view.setTranslationX((vpWidth - positionOffsetPixels) * tag.xIn);
                                view.setTranslationY((vpWidth - positionOffsetPixels) * tag.yIn);
                            }
                        }
                    }
                }

                //当前正在滑动的界面
                PageFragment outFragment = getPosition(position);
                if (outFragment != null) {
                    List<View> views = outFragment.getViews();
                    if (views != null && views.size() > 0) {
                        for (View view : views) {
                            AttrTagBean tag = (AttrTagBean) view.getTag();
                            if (tag != null) {
                                view.setTranslationX((0 - positionOffsetPixels) * tag.xOut);
                                view.setTranslationY((0 - positionOffsetPixels) * tag.yOut);
                            }
                        }
                    }
                }
            }

            @Override
            public void onPageSelected(int position) {
                //当划到最后一页时,小人的图标消失
                if (position == fragments.size() - 1) {
                    mInflaterIv.setVisibility(View.GONE);
                } else {
                    mInflaterIv.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {
                //这里是处理图中的小人的帧动画过程
                Drawable anim = mInflaterIv.getBackground();
                if (!(anim instanceof AnimationDrawable)) {
                    return;
                }
                AnimationDrawable animation = (AnimationDrawable) anim;
                Log.d("滑动状态", state + "");
                switch (state) {
                    //空闲状态
                    case ViewPager.SCROLL_STATE_IDLE:
                        animation.stop();
                        break;
                    //拖动状态
                    case ViewPager.SCROLL_STATE_DRAGGING:
                        animation.start();
                        break;
                    //惯性滑动状态
                    case ViewPager.SCROLL_STATE_SETTLING:
                        break;
                }
            }
        });

Demo

本文的所有代码已上传到CSDN的资源中心

Demo中包含两种方式实现本文的效果:

(1)自定义View方式

(2)自定义LayoutInflater.Factory

Android 中LayoutInflater(布局加载器)之实战篇Demo