在我们打开一个新的app时,几乎每个app都会有一个初始化动画显示,这个动画只显示一次,第二次打开就不会显示了,通常来说这个动画都在尽可能地向用户介绍这个app,也可以最大化的展示app的特点,亮点。比如这样:
它被称为平行动画,它的实现很简单,只是一个简单的ViewPager和一个ImageView控件而已。
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.splash.ParallaxContainer
android:id="@+id/parallax_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/iv_man"
android:layout_alignParentBottom="true"
android:layout_width="67dp"
android:layout_marginBottom="10dp"
android:layout_centerHorizontal="true"
android:layout_height="202dp"/>
</RelativeLayout>
它的布局文件内容仅此而已。
parallax_container.setUp(intArrayOf(
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_intro_6,
R.layout.view_intro_7,
R.layout.view_login
))
代码中将布局文件的ID全部传给自定义控件,自定义控件内又做了什么呢?
public void setUp(int[] ids){
for (int id : ids) {
ParallaxFragment fragment = new ParallaxFragment();
Bundle bundle = new Bundle();
bundle.putInt(LAYOUT_ID,id);
fragment.setArguments(bundle);
fragments.add(fragment);
}
adapter = new ParallaxAdapter(((AppCompatActivity) getContext()).getSupportFragmentManager(),
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,fragments);
ViewPager pager = new ViewPager(getContext());
pager.setId(R.id.parallax_viewpager);
pager.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
pager.addOnPageChangeListener(this); //添加事件回调
pager.setAdapter(adapter);
addView(pager); //其实这个自定义ViewGroup只有这么一个ViewPager
}
首先要为每个layout布局新建一个Fragment,以内每个Fragment都要显示一个布局文件,然后使用ViewPager来装载这些Fragment,最后可以发现,其实这个自定义ViewGroup(View容器)只有一个View,那就是ViewPager。ViewPager是一个谷歌官方提供的可用来对页面进行左右滑动的高级控件,与常见的ListView和RecyclerView类似,也需要一个适配器Adapter(适配器代码比较简单)
Fragment中也比较简单:
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Bundle bundle = getArguments();
if(bundle != null) {
int layoutId = bundle.getInt(ParallaxContainer.LAYOUT_ID);
if(layoutId != 0){
return inflater.inflate(layoutId,null);
}
}
return null;
}
到了这一步就完成了。但是。。还没有设置页面中ImageView的动画效果,如果对每一个ImageView都单独设置动画效果,显得比较繁琐(如果有几百个。。。。后果不堪设想),可以通过直接为ImageView添加属性:
<ImageView
android:id="@+id/iv_7"
android:layout_width="260dp"
android:layout_height="67dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="44dp"
android:src="@drawable/intro3_item_7"
app:a_in="1"
app:a_out="1"
app:x_in="1"
app:x_out="1" />
这样直接设置肯定是不行的,还需要在values文件夹中新建一个attrs文件,这个文件可以用来自定义属性,文件名很特殊,系统可以根据这个特定的文件找到自定义属性:
<resources>
<attr name="x_in" format="float"/>
<attr name="x_out" format="float"/>
<attr name="y_in" format="float"/>
<attr name="y_out" format="float"/>
<attr name="a_in" format="float"/>
<attr name="a_out" format="float"/>
</resources>
在Fragment的onCreateView方法中,正常来说都会直接
return inflater.inflate(layoutId,null);
就完事了,但是并没有对View的自定义属性进行处理,因此只在这里设置是没用的。重写LaoutInflater方法:
public ParallaxLayoutInflater(LayoutInflater original, Context newContext,ParallaxFragment fragment) {
super(original, newContext);
this.fragment = fragment;
setFactory2(new MyFactory(this));
}
首先LayoutInflater在将xml文件转换为View的过程中会回调Factory2接口,因此可以重新实现这个接口:
private class MyFactory implements Factory2 {
private LayoutInflater inflater;
private final String[] PREFIX = new String[]{
"android.widget.",
"android.view."
};
private MyFactory(LayoutInflater inflater){
this.inflater = inflater;
}
private final int[] attrsID = new int[]{
R.attr.x_in,
R.attr.x_out,
R.attr.y_in,
R.attr.y_out,
R.attr.a_in,
R.attr.a_out
};
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
//name是控件名,系统控件如:ImageView、TextView
//自定义控件如:xx.xx.xx.MyView
View view = createMyView(name,attrs);
if(view != null){
TypedArray array = context.obtainStyledAttributes(attrs,attrsID);
if(array.length() != 0) {
ParallaxTag tag = new ParallaxTag();
tag.xIn = array.getFloat(0, 0f);
tag.xOut = array.getFloat(1, 0f);
tag.yIn = array.getFloat(2, 0f);
tag.yOut = array.getFloat(3, 0f);
tag.aIn = array.getFloat(4, 0f);
tag.aOut = array.getFloat(5, 0f);
view.setTag(tag);
}
fragment.addView(view);
array.recycle();
}
return view;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return null;
}
public View createMyView(String name,AttributeSet attributeSet){
try {
if (name.contains(".")) { //自定义控件
return inflater.createView(name, null, attributeSet);
} else {
for (String prefix : PREFIX) {
View view = inflater.createView(name,prefix,attributeSet);
if(view != null){
return view;
}
}
}
}catch (ClassNotFoundException e){}
return null;
}
}
可以看出实现接口必须实现onCreateView方法,这个方法名已经十分明显了,对应xml文件中的每一个View都会回调一次这个方法,有多少个回调多少次,对应系统控件,回调参数name名称通常为:ImageView、TextView,而对于自定义控件,参数name名称则为xxx.yyy.MyView这样带有包名的名称。因此首先做判断,做不同的处理,使用inflater的createView来生成View。
还有一个很重要的参数就是attrs,这个参数包含了当前控件的所有属性信息,当然它也包括了自定义属性,如上面自定义的x_in,x_out等。获取这些属性,将他们保存在View的Tag中。
回到自定义控件中来,刚刚说到,它只有一个View,它就是ViewPager,ViewPager内部定义了许多回调方法:
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
ParallaxFragment outFragment = null;
try {
outFragment = fragments.get(position -1);
}catch (Exception e){}
if(outFragment != null){
List<View> outViews = outFragment.getViews();
if(outViews != null) {
for (View outView : outViews) {
ParallaxTag tag = (ParallaxTag) outView.getTag();
if (tag == null) {
continue;
}
outView.setTranslationX((getWidth() - positionOffsetPixels) * tag.xOut);
outView.setTranslationY((getWidth() - positionOffsetPixels) * tag.yOut);
}
}
}
ParallaxFragment inFragment = null;
try {
inFragment = fragments.get(position);
}catch (Exception e){}
if(inFragment != null){
List<View> inViews = inFragment.getViews();
if(inViews != null) {
for (View inView : inViews) {
ParallaxTag tag = (ParallaxTag) inView.getTag();
if (tag == null) {
continue;
}
inView.setTranslationX(( - positionOffsetPixels) * tag.xIn);
inView.setTranslationY( (- positionOffsetPixels) * tag.yIn);
}
}
}
}
@Override
public void onPageSelected(int position) {
if(position == fragments.size() - 1){
iv_man.setVisibility(GONE);
}else {
iv_man.setVisibility(VISIBLE);
}
}
@Override
public void onPageScrollStateChanged(int state) {
AnimationDrawable drawable = (AnimationDrawable) iv_man.getBackground();
switch (state){
case ViewPager.SCROLL_STATE_DRAGGING: //当ViewPager在滚动时
drawable.start();
break;
case ViewPager.SCROLL_STATE_IDLE: //当ViewPager停止滚动时
drawable.stop();
break;
}
}
onPageScrolled表示在滑动过程中会调用这个方法,这个方法会被调用若干次(滑动过程中,调用很多很多次),在方法中,首先可以得到滑动时即将离开屏幕的Fragment(简称outFragment)和即将进入的Fragment(简称inFragment)
如图,以向左滑动为例,在滑动过程中,分别遍历outFragment和inFragment中所有的View,然后获取他们对应的Tag,Tag中保存着他们对应的动画执行名称,这个可以自己设置,接着执行相应的设置方法就行了。
比如setTranslationX。因为onPageScrolled被回调的次数很多,很快,所以看起来就是一个动画了,其实只不过是调用了很多很多次setTranslationX,况且每次的传入参数都不一样而已。
onPageSelected表示当前Page。
onPageScrollStateChanged表示滑动的状态改变,如:
SCROLL_STATE_DRAGGING表示正在滑动,
SCROLL_STATE_IDLE表示停止滑动.
demo地址:https://github.com/lyx19970504/Splash