模仿知乎安卓客户端的banner广告条以及一些思考

本文记录模仿知乎安卓客户端的banner广告条的这个过程,以及这个过程中发现的一些有趣的东西

首先来看一下这个效果是什么样的.

效果分析

  1. 类似于viewpager的操作逻辑,左右滑动可以切换不同的页面.
  2. 切换的的过程是上一个页面渐隐,后一张页面渐显
  3. 页面没有发生位移.

资料搜索

方法一 继承ViewPager,重写onPageScrolled

这是我模仿鸿洋_的那篇博客里的自定义的ViewPager改写的

public class MyViewPager extends ViewPager {
 
   
    /**
     * 保存position与对于的View
     */
    private HashMap<Integer, View> mChildrenViews = new LinkedHashMap<Integer, View>();
    /**
     * 滑动时左边的元素
     */
    private View mLeft;
 
    /**
     * 滑动时右边的元素
     */
    private View mRight;

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void onPageScrolled(int position, float positionOffset,
                               int positionOffsetPixels) {
        
        /*获取左边的View*/
        mLeft = findViewFromObject(position);
        /*获取右边的View*/
        mRight = findViewFromObject(position + 1);

        /* 添加切换动画效果*/
        animateStack(mLeft, mRight, positionOffset, positionOffsetPixels);
        super.onPageScrolled(position, positionOffset, positionOffsetPixels);

    }

    public void setObjectForPosition(View view, int position) {
        mChildrenViews.put(position, view);
    }

    /**
     * 通过过位置获得对应的View
     *
     * @param position
     * @return
     */
    public View findViewFromObject(int position) {

        return mChildrenViews.get(position);
    }

    /*实现知乎banner效果*/
    protected void animateStack(View left, View right, float effectOffset,
                                int positionOffsetPixels) {
        if (left != null) {
            left.setTranslationX(positionOffsetPixels);
            left.setAlpha(1 - effectOffset);
            left.bringToFront();
        } 
     
        if (right != null) {
            right.setAlpha(effectOffset);
            right.setTranslationX(-getWidth() - getPageMargin() + positionOffsetPixels;);
           
        } 

    }
}

刚开始对于为什么要加一个HashMap来保存所有的页面存有疑问,就是这个段代码

    /**
     * 保存position与对于的View
     */
    private HashMap<Integer, View> mChildrenViews = new LinkedHashMap<Integer, View>();
    
    public void setObjectForPosition(View view, int position) {
        mChildrenViews.put(position, view);
    }
    
    /**
     * 通过过位置获得对应的View
     *
     * @param position
     * @return
     */
    public View findViewFromObject(int position) {
        return mChildrenViews.get(position);
    }

为什么不直接用getChildAt(position)方法直接获取多好,我再次看了鸿洋_的那篇博客,发现他已经把这种情况说明了.

1、【错误】我通过getChildAt(position),getChildAt(position+1),getChildAt(position-1)获得滑动时,左右的两个View;乍一看,还真觉得不错,在代码写出来,再乍效果也出不来 错误原因:我们忽略一个特别大的东西,ViewPager的机制,滑动时动态加载和删除View,ViewPager其实只会维持2到3个View,而position的范围基本属于无限~~

我查看了getChildAt(position)方法,发现viewPager里储存view的是一个view数组View[] mChildren.而且由于ViewPager最多只会维护1+2*mOffscreenPageLimit个页面,默认的mOffscreenPageLimit是1,也就是说默认情况下只会维护当前的页面加上左边的mOffscreenPageLimit个页面和右面的mOffscreenPageLimit个页面.
比如说我们的ViewPager下有三个页面A B C.

  • 刚打开时,mChildren里的情况是{A,B}
  • 向左滑动,B页面此时在中间显示.A,C则分别在左右两边里隐藏,mChildren的情况是{A,B,C}
  • 再次向左滑动,mChildren的情况是{B,C},
  • 向右滑动,mChildren的情况是{B,C,A}
  • 再次向右滑动,回到刚开始时,view数组里的情况是{B,A}

可以看到经过一番滑动,虽然刚开始和最后的页面显示是一样的,但是mChildren储存的页面顺序已经发生了变化,这这样就能解释通过getChildAt()方法来获取左右页面view是不确定的.
所以要自己维护页面view.
这里看实现知乎banner的关键代码

/*实现知乎banner效果*/
    protected void animateStack(View left, View right, float effectOffset,
                                int positionOffsetPixels) {
        if (left != null) {
            /*使左view保持不动*/
            left.setTranslationX(positionOffsetPixels);
            /*使view的透明度发生变化*/
            left.setAlpha(1 - effectOffset);
            left.bringToFront();
        } 
     
        if (right != null) {
            right.setAlpha(effectOffset);
            /*使右view保持不动*/
            right.setTranslationX(-getWidth() - getPageMargin() + positionOffsetPixels;);
           
        } 

    }

知乎的banner里有一个特别的地方就是滑动的时候view是不动的,而原生的ViewPager在滑动的时候View是会跟随滑动的.
我们知道运动是相对的,那我们就可以使view在滑动的时候再反向移动同样的距离就可以了
left.setTranslationX(positionOffsetPixels); right.setTranslationX(-getWidth() - getPageMargin() + positionOffsetPixels);
这里面的参数positionOffsetPixels就是手指移动的像素距离

positionOffsetPixels
如果手指从右到左的滑动(切换到后一个):0-720
如果手指从左到右的滑动(切换到前一个):720-0
这里的720是屏幕的宽度

控制透明度变化即view.setAlpha(effectOffset);这里的effectOffset的取值很值得琢磨

effectOffset
如果手指从右到左的滑动(切换到后一个):0.0~1.0,即从一半到最大
如果手指从左到右的滑动(切换到前一个):1.0~0,即从最大到一半

注意,比如我从右到左滑动,前一个页面的透明度是1.00.0,后一个透明度是0.01.0
所以我设置的时候就是
left.setAlpha(1 - effectOffset); right.setAlpha(effectOffset);
最后的效果也挺好的.不过我注意到一点,在滑动的时候会view会出现抖动.

方法二 ViewPager自带的动画方法viewPager.setPageTransformer()

这个方法就要灵活的多了,也方便移植.

 mViewPager.setPageTransformer(true, new ViewPager.PageTransformer() {
            @Override
            public void transformPage(View view, float position) {
             
            }
        });

我们先看一下源代码里是怎么讲的

    /**
     * Sets a {@link PageTransformer} that will be called for each attached page whenever
     * the scroll position is changed. This allows the application to apply custom property
     * transformations to each page, overriding the default sliding behavior.
     *
     * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
     * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.
     * By default, calling this method will cause contained pages to use
     * {@link ViewCompat#LAYER_TYPE_HARDWARE}. This layer type allows custom alpha transformations,
     * but it will cause issues if any of your pages contain a {@link android.view.SurfaceView}
     * and you have not called {@link android.view.SurfaceView#setZOrderOnTop(boolean)} to put that
     * {@link android.view.SurfaceView} above your app content. To disable this behavior, call
     * {@link #setPageTransformer(boolean,PageTransformer,int)} and pass
     * {@link ViewCompat#LAYER_TYPE_NONE} for {@code pageLayerType}.</p>
     *
     * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
     *                            to be drawn from last to first instead of first to last.
     * @param transformer PageTransformer that will modify each page's animation properties
     */
    public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
        setPageTransformer(reverseDrawingOrder, transformer, ViewCompat.LAYER_TYPE_HARDWARE);
    }

意思就是当页面发生滚动时,给viewPager中每一个附上的page(也就是前面提到的View[] mChildren中保存的页面)页面应用一个PageTransformer来取代默认的滑动动画而实现自己的属性动画.这里还讲了,由于属性动画是API 11之后才有的,因此API 11之前应用次方法是不会有效果的.
第一个参数reverseDrawingOrder 意思就是选择正序还是反序显示页面,true 正序,false 反序
第二个参数transformer 就是自定义的动画
再来看一看这个PageTransformer到底是个什么东东
源代码

 /**
     * A PageTransformer is invoked whenever a visible/attached page is scrolled.
     * This offers an opportunity for the application to apply a custom transformation
     * to the page views using animation properties.
     *
     * <p>As property animation is only supported as of Android 3.0 and forward,
     * setting a PageTransformer on a ViewPager on earlier platform versions will
     * be ignored.</p>
     */
    public interface PageTransformer {
        /**
         * Apply a property transformation to the given page.
         *
         * @param page Apply the transformation to this page
         * @param position Position of page relative to the current front-and-center
         *                 position of the pager. 0 is front and center. 1 is one full
         *                 page position to the right, and -1 is one page position to the left.
         */
        void transformPage(View page, float position);
    }

在每一个visible/attached的页面发生滚动时就会调用PageTransformer来实现自定义的动画.
第一个参数 View page,要设置的页面
第二个参数 float position,
position=0 页面刚好就在viewpager的中间
position=1 这个页面在右面
position=-1 这个页面在左面
看这个图 A B C是viewPager中三个不同的页面

那么滑动的时候呢
显而易见
从左到右滑动position的值会变大
从右到左滑动posotion的值会变小
一个页面的position在(-1,1)的区间这个页面才是看见的,其他的值都是不可见的.
这个参数很有意思,也不是很好理解.我也是经过了多次实验才真正弄明白了.
那么这个position的值到底是怎么来的呢.
查看一下源代码
原来这个transformPage(page,position)方法是在onPageScrolled()中被调用的
下面就是在onPageScrolled()中找到的

 if (mPageTransformer != null) {
            final int scrollX = getScrollX();
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                if (lp.isDecor) continue;
                final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
                mPageTransformer.transformPage(child, transformPos);
            }
        }

可以看到position的值就是final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();这样得到的

我先解释一个上式中的几个值的意思
child.getLeft()获得的就是该页面的左边到父布局的左边的距离,上图中
A页面的child.getLeft()为0
B页面的child.getLeft()为1080.
C页面的child.getLeft()为2160.
(这里1080为屏幕的宽度,2160即2x1080)
scrollx为viewPager的水平滑动的距离
以上图为例,scrollx=1080
若是B页面滑动到了左边,则scrollx为2160
getClientWidth()为获得页面的宽度.这里为屏幕的宽度1080

我们可以来验证一下
还是以上图为例
A页面的position为(0-1080)/1080=-1
B页面的position为(1080-1080)/1080=0
C页面的position为(2160-1080)/1080=1
明白了position的意思,我们就好进行下一步.
自定义我们自己的PageTransformer

        mViewPager.setPageTransformer(true, new ViewPager.PageTransformer() {
            @Override
            public void transformPage(View view, float position) {
                int pageWidth = view.getWidth();
                float positionOffsetPixels = pageWidth * position;
                float mTrans = -pageWidth + positionOffsetPixels;
                
                if (position < -1) { /* [-Infinity,-1)*/
                /*页面已经在屏幕左侧且不可视*/

                } else if (position <= 0) { /* [-1,0]*/
                    /*页面从左侧进入或者向左侧滑出的状态*/
                    view.setAlpha(1 + position);
                    view.setTranslationX(pageWidth * -position);

                } else if (position <= 1) {/* (0,1]*/
                    /*页面从右侧进入或者向右侧滑出的状态*/
                    view.setTranslationX(pageWidth * -position);
                    view.setAlpha(1 - position);
                }else if (position >1){
                /*页面已经在屏幕右侧且不可视*/
                
                }
            }
        });

设置效果的原理和第一种方法里是类似的.
有点不同的是transformPage(View view, float position)没有给我们提供位移的距离,不过这也难不倒我们.我们已经知道position是怎么算出来的,反推出位移距离也不是难事,即pageWidth * -position
来看一下我们实现的效果

好了,本文如果有疏漏和错误之处,请大家赐教.
下台鞠躬~