平行动画(模仿小红书引导页)

本文介绍了一种在Android应用中实现视差滚动效果的方法。通过使用Viewpager和Fragment结合自定义属性,使得不同控件在滑动时产生视觉差,营造炫酷的过渡动画。文章详细介绍了自定义FrameLayout、Fragment及LayoutInflater的过程。

其实之前在很多APP的引导页上都看到过这个效果,通过速度不同给人带来视觉差,感觉很炫酷。

通过网易云的课做了一个demo,记录一下。

先上图

首先来梳理一下实现的思路,其实就是viewpager+fragment,只不过这个fragment里的控件,会根据viewpager的滑动产生不同的速度的位移。从架构的角度来考虑的话,我们要做到可以随时添加fragment,同时可以随时添加fragment里的控件,控件的速度可以设置。所以要给系统控件来添加自定义属性。

attrs文件

<attr name="a_in" format="float" />//进入的时候透明度
<attr name="a_out" format="float" />//出去的时候透明度
<attr name="x_in" format="float" />//进入的时候x方向的速度
<attr name="x_out" format="float" />//出去的时候x方向的速度
<attr name="y_in" format="float" />//进入的时候Y方向的速度
<attr name="y_out" format="float" />出去的时候Y方向的速度

然后是布局文件其中的控件,添加我们自定义的属性

<ImageView
    android:id="@+id/iv_0"
    android:layout_width="103dp"
    android:layout_height="19dp"
    android:layout_centerInParent="true"
    android:src="@drawable/intro1_item_0"
    app:x_in="1.2"
    app:x_out="1.2" />

自定义FrameLayout

为了方便管理fragment和viewpager,我们自定义一个frameLayout,实现Viewpager的OnPageChangeListener,提供一个setUp方法,来添加布局文件,从而新建数量相同的fragment。

public void setUp(int... childIds) {

    //fragment集合
    fragments = new ArrayList<ParallaxFragment>();
    for (int i = 0; i < childIds.length; i++) {
        ParallaxFragment fragment = new ParallaxFragment();
        //通过bundle传递参数给fragment
        Bundle bundle = new Bundle();
        bundle.putInt("layoutId", childIds[i]);
        fragment.setArguments(bundle);
        fragments.add(fragment);
    }


    ViewPager vp = new ViewPager(getContext());
    vp.setId(R.id.parallax_pager);//从value里的ids拿的

    SplashActivity activity = (SplashActivity) getContext();
    parallaxPagerAdapter = new ParallaxPagerAdapter(activity.getSupportFragmentManager(), fragments);
    vp.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    vp.setAdapter(parallaxPagerAdapter);
    vp.setOnPageChangeListener(this);
    addView(vp,0);
}

 

自定义Fragment

 

public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

    //获取到布局文件id
    int rootViewId = getArguments().getInt("layoutId");
    //自定义LayoutInflater来处理里面的控件
    ParallaxLayoutInflater parallaxLayoutInflater = new ParallaxLayoutInflater(inflater,getActivity(),this);
    View rootView = parallaxLayoutInflater.inflate(rootViewId, null);

    return rootView;
}

自定义LayoutInflater

对于我们自定义的fragment ,我们同样需要通过自定义layoutInflater来对控件的布局进行管理,自定义的layoutInflater里最重要的就是要实现setFactory2这个方法,自定义一个Factory,通过里面的onCreateView方法,来获取控件的自定义属性,再把所有的自定义属性都通过ParallaxTag添加到View的tag里。这里要注意区分是系统控件还是自定义控件,也要考虑兼容自定义控件。

class ParallaxFactory implements Factory2 {

    private LayoutInflater layoutInflater;
    //系统控件的前缀
    private String[] sClassPrefix = {
            "android.widget.",
            "android.view."
    };
    //系统控件自定义的属性
    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};


    public ParallaxFactory(LayoutInflater layoutInflater) {
        this.layoutInflater = layoutInflater;
    }

    /**
     * 反射机制
     *
     * @param parent  顶级容器
     * @param name    控件名字(像RelativeLayout,ImageView)如果是自定义控件的话  返回的是全路径名字
     * @param context
     * @param attrs   控件的属性(width,height)
     * @return
     */
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

        View view = null;

        view = createMyView(name, context, attrs);

        if (view != null) {

            //attrs 控件的所有属性,attrIds 是想要获取的控件的属性
            TypedArray array = context.obtainStyledAttributes(attrs, attrIds);
            if (array != null && array.length() > 0) {
                ParallaxViewTag viewTag = new ParallaxViewTag();
                viewTag.alphaIn = array.getFloat(0, 0f);
                viewTag.alphaOut = array.getFloat(1, 0f);
                viewTag.xIn = array.getFloat(2, 0f);
                viewTag.xOut = array.getFloat(3, 0f);
                viewTag.yIn = array.getFloat(4, 0f);
                viewTag.yOut = array.getFloat(5, 0f);
                view.setTag(R.id.parallax_view_tag, viewTag);

            }
            fragment.getParallaxViews().add(view);
            array.recycle();
        }

        return view;
    }

    /**
     * 创建view
     * @param name
     * @param context
     * @param attrs
     * @return
     */
    private View createMyView(String name, Context context, AttributeSet attrs) {

        //如果是系统的控件,不会有.  如果是自定义控件的话  含有.
        if (name.contains(".")) {
            return reflectView(name, null, attrs);
        } else {
            //循环两个包
            for (String prefix : sClassPrefix) {
                View view = reflectView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            }

        }

        return null;
    }

    /**
     * @param name   控件名字
     * @param prefix 控件前缀
     * @param attrs  控件属性
     * @return
     */
    private View reflectView(String name, String prefix, AttributeSet attrs) {
        try {
            //通过系统的layoutInflater创建视图,读取系统属性
            return layoutInflater.createView(name, prefix, attrs);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }


    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }
}

 

最后我们就要实现,根据viewpager的滑动来控制Fragment里不同控件的不同移动速度,这里要区分fragment是进入状态还是退出状态

 @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        //动画操作

        int containerWidth = getWidth();

        //获取到退出
        ParallaxFragment outFragment = null;
        try {
            outFragment = fragments.get(position - 1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //获取到进入的页面
        ParallaxFragment inFragment = null;
        try {
            inFragment = fragments.get(position);
        } catch (Exception e) {
        }


        if (outFragment != null) {
            //获取Fragment上所有的视图,实现动画效果
            List<View> inViews = outFragment.getParallaxViews();
//            动画
            if (inViews != null) {
                for (View view : inViews) {
                    ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
                    if (tag == null) {
                        continue;
                    }
                    ViewHelper.setTranslationX(view, (containerWidth - positionOffsetPixels) * tag.xIn);
                    ViewHelper.setTranslationY(view, (containerWidth - positionOffsetPixels) * tag.yIn);
                }

            }

        }
        if (inFragment != null) {
            List<View> outViews = inFragment.getParallaxViews();
            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);
                }
            }
        }


    }

最后总结一下的话,其实就是对于自定义控件的实现,要做到熟练,考虑实现过程的时候,要尽量做到可扩展性,而不是单独实现功能就可以。最后还是贴github 

https://github.com/wangxueshen/NetEaseDemo

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值