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

前言

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

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


导航

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

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

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

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

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

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

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是不相同的:

//mFactory2mFactory2.onCreateView(parent, name, context, attrs);//mFactoryview = 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//自定义Viewcom.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 getViews();    /**     * 添加元素     */    void addView(View view);}

Fragment中的实现过程:

public class PageFragment extends Fragment implements InflaterViewImpl {    private List views = new ArrayList<>();//**************篇幅原因省略了部分方法************************//     @Override    public List 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 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 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


拓展

其实当天下载的小红书的App 后,发现引导页,并不是实战篇的样子。

小红书引导页

不得不说,这种实现方式,博主感觉挺有灵性的,简洁而不失观赏性。

然后博主就高仿了一波,下面是 Demo 地址。

因为这个需求和最近写的主题无关,就不打算写博客了,所以直接放出链接了。

先声明,这个是有偿的,也不多就2元,如果有实际需求的同学,可以下载下支持一下博主。

高仿小红书引导页

更多相关文章

  1. Android高手进阶教程(十一)之----Android 通用获取Ip的方法(判断
  2. Android布局之相对布局——RelativeLayout
  3. 【Android 初学】3、控件布局初步
  4. Android ListView中点击单行实现RadioButton的单选功能,自定义Ite
  5. android五种布局-霓虹灯效果实现
  6. 通过对inflate与 findViewById 方法的探究深度了解一下Android
  7. Android 屏幕旋转生命周期以及处理方法
  8. Android实现在一个activity中添加多个listview的方法

随机推荐

  1. 包建强的培训课程(6):Android(安卓)App瘦身
  2. 移植ffmpeg库到Android后的简单使用
  3. Android通知的使用及设置
  4. Android(安卓)打开输入法,中文模式没有候
  5. Android 10正式版发布
  6. [Android]你不知道的Android进程化(5)--
  7. 交叉编译环境学习(编译Android上的可执行
  8. Android崩溃日志获取与解析
  9. Android 使用Glide4.9 压缩并保存图片(jp
  10. android 使用html做应用程序界面初探