View的绘制(5)-小红书欢迎页的视差效果实现

主目录见:Android高级进阶知识(这是总目录索引)

一.目标

看了上一篇《换肤框架(一)之Support v7库解析》想必大家很期待换肤框架(二),但是为什么冒死把这一篇提前呢?这里有个原因就是为了给大家巩固下support v7里面的知识点,以便到时能更容易理解换肤框架,如果你现在去看我等你十分钟。。。

《View的绘制(5)-小红书欢迎页的视差效果实现》 正经的程序员

大家想要源代码可以重击下载,当然这也不是原创代码了,但是这个地方可以用来说明这个知识点。

《View的绘制(5)-小红书欢迎页的视差效果实现》 效果图

二.代码分析

1.基础用法

这个地方用法很简单,直接看下面使用的代码:
1.1)第一步:在布局里面添加

 <com.lenovohit.redbookparallx.ParallxContainer
        android:id="@+id/parallax_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

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
        android:id="@+id/iv_0"
        android:layout_width="103dp"
        android:layout_height="19dp"
        android:layout_centerInParent="true"
        android:src="@mipmap/intro1_item_0"
        app:x_in="1.2"
        app:x_out="1.2" />

大家看下第一个页面里面的一个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已经自定义完毕。其实这就是这篇文章的关键点,为了讲解代码的完整性,这里还不算完,但是到这里我的目的已经达到,先休息五分钟。。。

《View的绘制(5)-小红书欢迎页的视差效果实现》 嗨起来

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<View> 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<View> 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的旅程中快乐,一起成长。

    原文作者:ZJ_Rocky
    原文地址: https://www.jianshu.com/p/7c6660da0f10
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞