其实之前在很多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(); 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 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 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); } } } }
最后总结一下的话,其实就是对于自定义控件的实现,要做到熟练,考虑实现过程的时候,要尽量做到可扩展性,而不是单独实现功能就可以。
源码地址:
https://github.com/wangxueshen/NetEaseDemo
到这里就结束啦.
往期精彩回顾:
Android实现短信验证码自动填充功能
Android仿echo精美弹幕功能
Android实现头像重叠排列功能
Android仿QQ个性标签功能
Android仿QQ侧滑删除的功能