Android工具箱(一):3D ViewPager

好久没有更新博客了,积攒了很多UI效果,准备整理一下写一个系列。

作为android前端程序员,最重要的就是在工作过程中不断丰富自己的类库,等要用的时候直接拿出来改改就行了,我把它命名为Android工具箱。

今天要实现的一个效果是一个有科技感的3D ViewPager,看图:

这里写图片描述

实现这个效果主要分两个部分:

一. 无限循环ViewPager

这个网上一搜一大把,无非就是下面两种实现方式:

  • 使得adapter的getCount()返回Integer.MAX_VALUE,然后在初始化的时候通过setCurrentItem()把当前页设置成一个中间值,这样左右就都有page了。然后在instantiateItem()的时候通过取模还原成真实的index。
  • 数据集前后各补一个(比如123补成31231),然后监听viewpager的滑动,在onPageScrollStateChanged() 里通过setCurrentItem()设置真正的页面index。

    第一种方法比较简单一些,而且逻辑清晰,github上有现成的封装:

    https://github.com/antonyt/InfiniteViewPager

    但是实际使用的时候发现会出现ANR,重现步骤如下:

    点击下方的viewpager页面,弹出上方的viewpager。左右滑动几次,按back回到下方的viewpager的时候,需要调用setCurrentItem()使得上下选择的页面一致,此时会出现ANR。究其原因,主要跟viewpager的代码实现相关:

    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
       ... ...
       if (mFirstLayout) {
           mCurItem = item;
           if (dispatchSelected) {
               dispatchOnPageSelected(item);
           }
           requestLayout();
       } else {
           populate(item);
           scrollToItem(item, smoothScroll, velocity, dispatchSelected);
       }
    }

    可以看到,第一次layout的时候会进上面那个代码块,但是我们back回来的时候会进下面那个代码块调用populate()方法,问题就出在这个populate()方法上:

    void populate(int newCurrentItem) {
        ... ...
       final int N = mAdapter.getCount();
        ... ...
               for (int pos = mCurItem + 1; pos < N; pos++) {
                   ... ...
               }
       ... ...
    }

    明白了吧,这里边有个循环,循环次数跟getCount()相关,而我们的getCount()返回的是Integer.MAX_VALUE。。。

    修复这个问题有两种方法:

  • 一种是每次都通过反射把上面那个mFirstLayout设置成true,不让它走进populate()那个代码块。不过这终究是一种hack。

  • 另外一种就是改小getCount()的返回值啦,一般返回item数量的100~200倍就可以了,很少有无聊的人一直滑到这么多页的。当然,逻辑上这只是一种“伪无限循环”。

    这里采用了第二种解决方式,只需要改动一行:

       @Override
       public int getCount() {
           if (getRealCount() == 0) {
               return 0;
           }
           // warning: scrolling to very high values (1,000,000+) results in
           // strange drawing behaviour
           // warning: return Integer.MAX_VALUE may cause ANR when calling setCurrentItem(),
           // since there's a loop in ViewPager.popluate() referring to getCount().
           // return Integer.MAX_VALUE;
           return getRealCount() * 200;
       }

二. 滑动过程中的转换动画

Viewpager提供了PagerTransformer用于实现转换动画,详情参见:

https://developer.android.com/training/animation/screen-slide.html

简而言之,滑动过程中每个page都会有一个不断更新position值:正中显示的page的位置为0,左边为负值,右边为正值。滑动完成后,左边不可见page的位置依次为-1,-2,到负无穷;右边不可见page的位置依次为+1,+2,到正无穷。我们需要做的就是重写transform()方法,根据里面的position值对相应的view进行处理。代码如下:

   public class RotateYTransformer implements ViewPager.PageTransformer {
       private float mMaxRotate = 25.0f;
       private OnTransformListener mOnTransformListener;

       public interface OnTransformListener {
           void onTransform(View page, float position);
       }

       ... ...

       @Override
       public void transformPage(View page, float position) {
           page.setPivotY(page.getHeight() / 2);

           if (position < -1) { // [-Infinity, -1)
               // This page is way off-screen to the left.
               page.setPivotX(page.getWidth());
               page.setRotationY(-1 * mMaxRotate);
           } else if (position <= 1) { // [-1,1]
               if (position < 0) { // [0, -1]
                   page.setPivotX(page.getWidth());
                   page.setRotationY(position * mMaxRotate);
               } else { // [1, 0]
                   page.setPivotX(0);
               }
               page.setRotationY(position * mMaxRotate);
           } else { // (1, +Infinity]
               // This page is way off-screen to the right.
               page.setPivotX(0);
               page.setRotationY(1 * mMaxRotate);
           }

           if (mOnTransformListener != null) {
               mOnTransformListener.onTransform(page, position);
           }
       }
   }

代码很简单,就是右边的page以0为原点沿Y轴旋转,左边的page以page width为原点沿Y轴旋转。

另外还定义了一个OnTransformListener,主要是用来高亮当前页的。之前试过用OnPageChangeListener来高亮当前页,但是效果很不自然,在手松开的一刹那突然高亮,用户体验不好,因此就改用了这种方式,在MainActivity里进行设置:

   RotateYTransformer transformer = new RotateYTransformer();
   transformer.setOnTransformListener(new RotateYTransformer.OnTransformListener() {
       @Override
       public void onTransform(View page, float position) {
           if (position < 0.5f && position >-0.5f) {
               page.setBackground(
                 getResources().getDrawable(R.drawable.viewpager_item_bg_highlight));
           } else {
               page.setBackground(
                 getResources().getDrawable(R.drawable.viewpager_item_bg));
           }
       }
   });
   mViewPager.setPageTransformer(false, transformer);

最后还有个问题,怎么让左右两边的page露出来一点让用户可以看到?首先我们需要给viewpager设置一个margin,留出绘制的空间。然后我们需要给viewpager以及它的的父容器设置一个android:clipChildren=”false”的属性,告诉父容器超出view返回的内容不要裁剪掉,继续绘制,这样就可以看到左右两边的page了。实际过程中有可能发现设置了clipChildren属性还是不起作用,这时候需要检查是不是父容器上面还有父容器,所有的容器都需要设置上这个属性。

三. 一些其他问题

基本效果已经实现了,当然还不完美。你可能会发现,只有中间的page可以接收touch事件,两边的page虽然露出来一点,但是你是没办法滑动它们的。要解决这个问题,最直接的思路是把margin换成padding,这样就可以接收touch事件了,当然还需要给父容器加上android:clipToPadding=”false”这个属性来告诉父容器不要对padding部分进行裁剪。不过如果你尝试过的话就会发现改完后transform()里获得的position会有问题,所有的页面都偏掉了。。。要修复这个问题,需要对position进行修正:

   int padding = viewpager.getPaddingLeft();
   int pageWidth = viewpager.getWidth() - padding * 2;
   float posCorrection = -(float)padding / width;

然后在transform()方法里加上这个偏移量对position进行修正就可以了。由于测试没有提这个问题,所以这部分代码没有提交上去。

源码下载地址:https://github.com/qianxin2016/ViewPager3D

猜你喜欢

转载自blog.csdn.net/turkeycock/article/details/78687549