主目录见:Android高级进阶知识(这是总目录索引)
一.目标
看了上一篇《换肤框架(一)之Support v7库解析》想必大家很期待换肤框架(二),但是为什么冒死把这一篇提前呢?这里有个原因就是为了给大家巩固下support v7里面的知识点,以便到时能更容易理解换肤框架,如果你现在去看我等你十分钟。。。
大家想要源代码可以重击下载,当然这也不是原创代码了,但是这个地方可以用来说明这个知识点。
二.代码分析
1.基础用法
这个地方用法很简单,直接看下面使用的代码:
1.1)第一步:在布局里面添加
1.2)第二步:给这个ParallxContainer 设置页面
ParallxContainer container = (ParallxContainer) findViewById(R.id.parallax_container);
container.setUp(
new int[]{
R.layout.view_intro_1,
R.layout.view_intro_2,
R.layout.view_intro_3,
R.layout.view_intro_4,
R.layout.view_intro_5,
R.layout.view_login
}
);
//设置动画
ImageView iv_man = (ImageView) findViewById(R.id.iv_man);
iv_man.setBackgroundResource(R.drawable.man_run);
container.setIv_man(iv_man);
但是这个地方布局里面会有自定义的属性,举一个页面view_intro_1说明一下:
大家看下第一个页面里面的一个ImageView添加了 app:x_in, app:x_out,这里ImageView原本是没有这个属性的,那么我们要怎么去识别这些属性并且能设置给ImageView呢?答案就是我们会自定义一个继承LayoutInflater的类去拦截View的创建过程。如果不知道这个知识点的可以倒带到《换肤框架(一)之Support v7库解析》里面讲的非常清楚。
2.自定义LayoutInflater
自定义LayoutInflater的话要去继承LayoutInflater,然后会强制你实现他的构造方法和cloneInContext(Context context)方法。
1.首先我们看下cloneInContext(Context context)方法:
@Override
public LayoutInflater cloneInContext(Context context) {
return new ParallaxLayoutInflater(this,context,fragment);
}
我们看到这个方法就是返回一个LayoutInflater 对象,所以我们把我们的自定义LayoutInflater对象返回即可。
2.然后我们看下构造方法:
protected ParallaxLayoutInflater(LayoutInflater original, Context newContext,ParallaxFragment fragment) {
super(original, newContext);
this.fragment = fragment;
//重新设置布局加载器的工厂
//工厂:创建布局文件中所有的视图
LayoutInflaterCompat.setFactory(this, new ParallaxFactory(this));
}
我们看到这里有一句关键代码,就是调用LayoutInflaterCompat的setFactory方法,其实在LayoutInflater中就有setFactory方法,为什么这个地方要调用LayoutInflaterCompat的setFactory方法呢?答案是兼容性,这个类是兼容包support里面的,不然在继承了AppCompatActivity之后会没有效果。
这个地方重新提下为什么要设置Factory,原因很简单就是setContentView()源码分析里面有句话:
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
可以看出只要我们设置了我们自定义的Factory然后重写onCreateView方法就可以拦截View的创建过程了,这样就可以拦截到前面说的ImageView没有的 app:x_in, app:x_out属性,达到设置给ImageView的目的。
3.自定义factory
前面我们看到我们要设置一个自己的Factory,那么这个Factory怎么自定义呢?首先第一步要继承LayoutInflaterFactory,然后重写onCreateView方法,这也是前面说过的。方法如下:
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = null;
if (view == null) {
view = createViewOrFailQuietly(name,context,attrs);
}
//实例化完成
if (view != null) {
//获取自定义属性,通过标签关联到视图上
setViewTag(view,context,attrs);
fragment.getViews().add(view);
}
return view;
}
我们看到createViewOrFailQuietly这一句,这个方法就是创建一个View,方法是我们自己实现的,方法如下:
private View createViewOrFailQuietly(String name, Context context,
AttributeSet attrs) {
//1.自定义控件标签名称带点,所以创建时不需要前缀
if (name.contains(".")) {
createViewOrFailQuietly(name, null, context, attrs);
}
//2.系统视图需要加上前缀
for (String prefix : sClassPrefix) {
View view = createViewOrFailQuietly(name, prefix, context, attrs);
if (view != null) {
return view;
}
}
return null;
}
方法的意思就是
1.如果是自己自定义的控件的话就不用加上前缀,为什么呢?原因是因为我们自定义控件写进xml文件的时候是包名+类名例如com.lenovohit.redbookparallx.ParallxContainer,这样在系统反射创建这个类的时候是可以成功的。
2.而如果是系统控件如ImageVIew是没有全称的,所以我们需要加上前缀,就是包名,这个我们得看我们用到的几个控件分别在哪几个包里面然后放进sClassPrefix数组里,最后分别遍历去尝试创建。
具体的创建代码如下:
private View createViewOrFailQuietly(String name, String prefix, Context context,
AttributeSet attrs) {
try {
//通过系统的inflater创建视图,读取系统的属性
return inflater.createView(name, prefix, attrs);
} catch (Exception e) {
return null;
}
}
这个地方的inflater就是我们自己自定义的LayoutInflater,当然最后创建完视图,我们需要将我们自定义的属性和视图View关联起来,以便于我们在滑动的时候可以取出来设置。
所以我们最后调用了:
//实例化完成
if (view != null) {
//获取自定义属性,通过标签关联到视图上
setViewTag(view,context,attrs);
fragment.getViews().add(view);
}
这里面的setViewTag就是取出自定义属性,然后将它设置给View:
private void setViewTag(View view, Context context, AttributeSet attrs) {
//所有自定义的属性
int[] attrIds = {
R.attr.a_in,
R.attr.a_out,
R.attr.x_in,
R.attr.x_out,
R.attr.y_in,
R.attr.y_out};
//获取
TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
if (a != null && a.length() > 0) {
//获取自定义属性的值
ParallaxViewTag tag = new ParallaxViewTag();
tag.alphaIn = a.getFloat(0, 0f);
tag.alphaOut = a.getFloat(1, 0f);
tag.xIn = a.getFloat(2, 0f);
tag.xOut = a.getFloat(3, 0f);
tag.yIn = a.getFloat(4, 0f);
tag.yOut = a.getFloat(5, 0f);
//index
view.setTag(R.id.parallax_view_tag,tag);
}
a.recycle();
}
到这里我们的自定义LayoutInflater已经自定义完毕。其实这就是这篇文章的关键点,为了讲解代码的完整性,这里还不算完,但是到这里我的目的已经达到,先休息五分钟。。。
4.Fragment onCreateView
我们自定义完我们的LayoutInflater,那我们要使用呀,这篇文章的效果因为我们是用ViewPager里面放置Fragment,所以我们的一页一页布局都是在Fragment里面,所以我们在Fragment的onCreateView中创建视图的时候用上它:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle bundle = getArguments();
int layoutId = bundle.getInt("layoutId");
ParallaxLayoutInflater layoutInflater = new ParallaxLayoutInflater(inflater, getActivity(),this);
return layoutInflater.inflate(layoutId, null);
}
我们自定义的ParallaxLayoutInflater 派上用场了,这个地方我们在创建视图的时候就会走我们自己的LayoutInflater了,内心是不是无比激动。
5.ParallxContainer setUp
我们开头在基础用法里面看到我们用的时候会调用这个方法,那么我们来看这个方法做了啥:
public void setUp(int... childIds) {
fragments = new ArrayList<>();
for (int i = 0; i < childIds.length; i++) {
ParallaxFragment f = new ParallaxFragment();
Bundle args = new Bundle();
//页面索引
args.putInt("index", i);
//Fragment中需要加载的布局文件id
args.putInt("layoutId", childIds[i]);
f.setArguments(args);
fragments.add(f);
}
//实例化适配器
SplashActivity activity = (SplashActivity)getContext();
adapter = new ParallaxAdapter(activity.getSupportFragmentManager(), fragments);
//实例化ViewPager
ViewPager vp = new ViewPager(getContext());
vp.setId(R.id.parallax_pager);
vp.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
//绑定
vp.setAdapter(adapter);
addView(vp,0);
vp.addOnPageChangeListener(this);
}
到现在大家应该都看得懂这段代码,就是创建出几个Fragment,然后实例化适配器给ViewPager,最后设置了个ViewPager的监听。这个监听还蛮重要的,因为我们绑定了View和自定义的属性之后,在这边要取出来真正根据自定义的属性来设置动画呀。
6.ParallxContainer onPageScrolled
我们的ParallxContainer 实现了OnPageChangeListener接口,所以这几个方法也在这个类里面重写了。第一个是滑动时候要做的事情:
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
this.containerWidth = getWidth();
//在翻页的过程中,不断根据视图的标签中对应的动画参数,改变视图的位置或者透明度
//获取到进入的页面
ParallaxFragment inFragment = null;
try {
inFragment = fragments.get(position - 1);
} catch (Exception e) {}
//获取到退出的页面
ParallaxFragment outFragment = null;
try {
outFragment = fragments.get(position);
} catch (Exception e) {}
if (inFragment != null) {
//获取Fragment上所有的视图,实现动画效果
List inViews = inFragment.getViews();
if (inViews != null) {
for (View view : inViews) {
//获取标签,从标签上获取所有的动画参数
ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
if (tag == null) {
continue;
}
//translationY改变view的偏移位置,translationY=100,代表view在其原始位置向下移动100
//仔细观察进入的fragment中view从远处过来,不断向下移动,最终停在原始位置
ViewHelper.setTranslationY(view, (containerWidth - positionOffsetPixels) * tag.yIn);
ViewHelper.setTranslationX(view, (containerWidth - positionOffsetPixels) * tag.xIn);
}
}
}
if(outFragment != null){
List outViews = outFragment.getViews();
if (outViews != null) {
for (View view : outViews) {
ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
if (tag == null) {
continue;
}
//仔细观察退出的fragment中view从原始位置开始向上移动,translationY应为负数
ViewHelper.setTranslationY(view, 0 - positionOffsetPixels * tag.yOut);
ViewHelper.setTranslationX(view, 0 - positionOffsetPixels * tag.xOut);
}
}
}
}
这段代码注释还是比较清楚的,我在这里简单说一下逻辑,这个地方就是取出前一个fragment和后一个要进入的fragment,然后遍历这个fragment里面的视图控件,根据刚才视图控件绑定的属性来设置该控件的动画。
7.ParallxContainer onPageSelected
这个方法很简单,就是看目前处于第几个页面来判断中间走路的女生要不要显示:
@Override
public void onPageSelected(int position) {
if (position == adapter.getCount() - 1) {
iv_man.setVisibility(INVISIBLE);
}else{
iv_man.setVisibility(VISIBLE);
}
}
8.ParallxContainer onPageScrollStateChanged
下面这个方法也很简单,就是判断ViewPager处于什么状态,跟着这个女生要开始走动还是不走动,这个地方的动画效果是用帧动画来做的。
@Override
public void onPageScrollStateChanged(int state) {
AnimationDrawable animation = (AnimationDrawable) iv_man.getBackground();
switch (state) {
case ViewPager.SCROLL_STATE_DRAGGING:
animation.start();
break;
case ViewPager.SCROLL_STATE_IDLE:
animation.stop();
break;
default:
break;
}
}
到这里我们就说完我们的代码了,其实除了那个知识点其他都是基础,想必不会难倒大家,如果哪里有说的不清楚的,大家可以留言说一下。Have a good journey!
总结:在这里依旧要总结一下,这个地方主要用到的知识点其实跟support v7库用到的是一模一样,所以很多知识是关联的,希望大家循序渐进,不骄不躁,在Android的旅程中快乐,一起成长。