本文出自博客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)退出该界面时,界面上的物理快速飞出去。
实现思路
对于上述的分析,这里的实现思路存在两种:
自定义View,自定义xIn、yIn、xOut、yOut四个属性的系数,所有界面上的物体继承这个自定义View。
自定义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方法
在这里就简单描述下,这个方法的主要流程:
对一些特殊标签,做分别处理,例如:view,TAG_1995(blink)
进行对Factory、Factory2的设置判断,如果设置那么就会通过设置Factory、Factory2进行生成View
如果没有设置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
拓展
其实当天下载的小红书的App 后,发现引导页,并不是实战篇的样子。
小红书引导页
不得不说,这种实现方式,博主感觉挺有灵性的,简洁而不失观赏性。
然后博主就高仿了一波,下面是 Demo 地址。
因为这个需求和最近写的主题无关,就不打算写博客了,所以直接放出链接了。
先声明,这个是有偿的,也不多就2元,如果有实际需求的同学,可以下载下支持一下博主。