敲酷炫的 ViewPager 切换效果和弹性指示器。

7,459 阅读13分钟

前言

前些天看到这个效果图
GIF




效果真是酷炫极了,感觉很biu踢,咱们说做就做。

[改装加强版,改进了圆入框的甩尾效果,最重要的一点是
增强ViewPager切换效果和卡片阴影]

效果图

集成方式【伸手党福利】

github地址 : github.com/qdxxxx/Bezi…
多谢老铁随手就是一个star,抱拳。
[标题党一般是: 转疯了,项目集成此酷炫动画只要3步!]

  • 注入依赖
    Step 1. Add the JitPack repository to your build file
    Step 2. Add the dependency
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
dependencies {
    compile 'com.github.qdxxxx:BezierViewPager:v1.0.5'
}


  • xml布局代码
    <qdx.bezierviewpager_compile.vPage.BezierViewPager
        android:id="@+id/view_page"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <qdx.bezierviewpager_compile.BezierRoundView
        android:id="@+id/bezRound"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
     />


  • Activity里面集成代码
 CardPagerAdapter cardAdapter = new CardPagerAdapter(getApplicationContext());
 cardAdapter.addImgUrlList(imgList);  //放置图片url的list

BezierViewPager viewPager = (BezierViewPager) findViewById(R.id.view_page);
viewPager.setAdapter(cardAdapter);

BezierRoundView bezRound = (BezierRoundView) findViewById(R.id.bezRound);
bezRound.attach2ViewPage(viewPager);



方法及属性介绍

  • BezierRoundView
nameformat中文解释
color_bezcolor贝塞尔圆球颜色
color_touchcolor触摸反馈
color_strokecolor圆框的颜色
time_animatorinteger动画时间
round_countinteger圆框数量,即Adapter.getCount
radiusdimension贝塞尔圆球半径,圆框半径为(radius-2)
attach2ViewPageBezierViewPager绑定指定的ViewPager(处理滑动时触摸事件)
并自动设置round_count


  • BezierViewPager[extends ViewPager]
nameformat中文解释
showTransformerfloatViewPager滑动到当前显示页的放大比例


  • CardPagerAdapter[extends PagerAdapter]
nameformat中文解释
addImgUrlListList包含图片地址的list
setOnCardItemClickListenerOnCardItemClickListener当前ViewPager点击事件
返回CurPosition
setMaxElevationFactorintegerAdapter里CardView最大的Elevation




实现解剖

1.从实现贝塞尔圆开始

[建议先看这篇文章]
贝塞尔圆



首先,我们需要绘制P0,然后 cubicTo p1,p2,p3,再cubicTo p4,p5.p6……
原谅我用这么简单粗暴的方式画圆…

    private PointF p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11;
        p0 = new PointF(0, -mRadius);//mRadius圆的半径
        p6 = new PointF(0, mRadius);

        p1 = new PointF(mRadius * bezFactor, -mRadius);//bezFactor即0.5519...
        p5 = new PointF(mRadius * bezFactor, mRadius);

        p2 = new PointF(mRadius, -mRadius * bezFactor);
        p4 = new PointF(mRadius, mRadius * bezFactor);

        p3 = new PointF(mRadius, 0);
        p9 = new PointF(-mRadius, 0);

        p11 = new PointF(-mRadius * bezFactor, -mRadius);
        p7 = new PointF(-mRadius * bezFactor, mRadius);

        p10 = new PointF(-mRadius, -mRadius * bezFactor);
        p8 = new PointF(-mRadius, mRadius * bezFactor);

再绘制path

        mPath.moveTo(p0.x, p0.y);
        mPath.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
        mPath.cubicTo(p4.x, p4.y, p5.x, p5.y, p6.x, p6.y);
        mPath.cubicTo(p7.x, p7.y, p8.x, p8.y, p9.x, p9.y);
        mPath.cubicTo(p10.x, p10.y, p11.x, p11.y, p0.x, p0.y);
        mPath.close();

圆
一个贝(ri)塞(ben)尔(guo)圆(qi)栩栩如生。


我们尝试通过手指滑动改变,p2,p3,p4的x轴坐标来观察圆的变化

@Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_DOWN:
                p2 = new PointF(event.getX() - mWidth / 2, -mRadius * bezFactor);
                p3 = new PointF(event.getX() - mWidth / 2, 0);
                p4 = new PointF(event.getX() - mWidth / 2, mRadius * bezFactor);

                invalidate();
                break;
        }

        return true;
    }

这里写图片描述这里写图片描述这里写图片描述


2.解剖效果图

贝塞尔圆

这里写图片描述 这里写图片描述 这里写图片描述


首先我们不考虑反弹效果,圆的变化有3种状态

  1. bezier圆还没离开圆框,p2,3,4 x轴坐标由 , 变化至 2r
  2. bezier圆离开圆框,至到达中心位置
    [p2,3,4 x轴坐标由 2r 变化至 1.5r ],[p8,9,10 x轴坐标由 变化至 1.5r ]
  3. bezier圆由中心位置,至到达下一个圆框。
    [p2,3,4, 8,9,10 x轴坐标由 1.5r 变化至 ]

老样子,我们用ValueAnimator来模拟一下[0,1]变化的值。【因为ViewPager的onPageScrolled监听中positionOffset是[0,1)变化的,类似。】

惊!下面几段代码居然!男的看了沉默,女的看了流泪。

    //展示动画
    private ValueAnimator animatorStart;
    private TimeInterpolator timeInterpolator = new DecelerateInterpolator();
    private float animatedValue; //[0,1]的值
    public void startAnimator() {
        if (animatorStart != null) {
            if (animatorStart.isRunning()) {
                return;
            }
            animatorStart.start();
        } else {
            animatorStart = ValueAnimator.ofFloat(0, 1f).setDuration(1500);
            animatorStart.setInterpolator(timeInterpolator);
            animatorStart.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    animatedValue = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            animatorStart.start();
        }
    }
    private float rRadio=1;  //P2,3,4 x轴倍数 
    private float lRadio=1;  //P8,9,10倍数
    private float tbRadio=1;  //y轴缩放倍数

    private float disL = 0.5f;   //离开圆的阈值
    private float disM = 0.8f;  //最大值的阈值
    private float disA = 0.9f;  //到达下个圆框的阈值
        if (0 < animatedValue && animatedValue <= disL) { //还没离开圆框的时候
            rRadio = 1f + animatedValue * 2;              //[1,2]
        }
        if (disL < animatedValue && animatedValue <= disM) {//离开圆框,至最大值区域
            rRadio = 2 - range0Until1(disL, disM) * 0.5f;          //  [2,1.5]
            lRadio = 1 + range0Until1(disL, disM) * 0.5f;          // [1,1.5]

        }
        if (disM < animatedValue && animatedValue <= disA) {    //从最大值,至到达下一个圆框
            rRadio = 1.5f - range0Until1(disM, disA) * 0.5f;     //  [1.5,1]
            lRadio = 1.5f - range0Until1(disM, disA) * 0.5f;     //  [1.5,1]

        }
    /**
     * 将值域转化为[0,1]
     *
     * @param minValue 大于等于
     * @param maxValue 小于等于
     * @return 根据当前 animatedValue,返回 [0,1] 对应的数值
     */
    private float range0Until1(float minValue, float maxValue) {
        return (animatedValue - minValue) / (maxValue - minValue);
    }

请再次原谅我用这么简单粗暴的方式画圆…

        mPath.moveTo(p0.x, p0.y * tbRadio);
        mPath.cubicTo(p1.x, p1.y * tbRadio, p2.x * rRadio, p2.y, p3.x * rRadio, p3.y);
        mPath.cubicTo(p4.x * rRadio, p4.y, p5.x, p5.y * tbRadio, p6.x, p6.y * tbRadio);
        mPath.cubicTo(p7.x, p7.y * tbRadio, p8.x * lRadio, p8.y, p9.x * lRadio, p9.y);
        mPath.cubicTo(p10.x * lRadio, p10.y, p11.x, p11.y * tbRadio, p0.x, p0.y * tbRadio);
        mPath.close();

理清了上面这些代码,一个有灵性的贝塞尔圆就即将绘制成功。我们再加上离开圆至到达下一个圆框这个区域y轴变化,[p,5,6,7, 1,0,11],效果就如下所示。
这里写图片描述

3.模拟效果

这时候我们已经将贝塞尔圆的运动方式给表达出来了,再加上一些效果[位移/反弹/翻转],我们就能模拟出贝塞尔圆从一个圆框进入下一个圆框的动画了。
在上面的基础上,我们加上反弹效果

        if (0 < animatedValue && animatedValue <= disL) { //还没离开圆框的时候
            rRadio = 1f + animatedValue * 2;              //[1,2]
        }
        if (disL < animatedValue && animatedValue <= disM) {//离开圆框,至最大值区域
            rRadio = 2 - range0Until1(disL, disM) * 0.5f;          //  [2,1.5]
            lRadio = 1 + range0Until1(disL, disM) * 0.5f;          // [1,1.5]
            tbRadio = 1 - range0Until1(disL, disM) / 3;           // [1 , 2/3]
        }
        if (disM < animatedValue && animatedValue <= disA) {    //从最大值,至到达下一个圆框
            rRadio = 1.5f - range0Until1(disM, disA) * 0.5f;     //  [1.5,1]
            lRadio = 1.5f - range0Until1(disM, disA) * (1.5f - boundRadio);      //反弹效果,进场 内弹boundRadio  lRadio =[1.5,boundRadio]
            tbRadio = (range0Until1(disM, disA) + 2) / 3;        // [ 2/3,1]   
        }
         if (disA < animatedValue && animatedValue <= 1f) {//到达圆框,lRadio=[boundRadio,1]
            rRadio = 1;
            tbRadio = 1;
            lRadio = boundRadio + range0Until1(disA, 1) * (1 - boundRadio);     //反弹效果,饱和
        }

再加上位移效果。一开始我在想,贝塞尔圆要不断的变化形态,还要移动位置。岂不相当的麻烦。后来把它分解成变化状态+不断位移效果。

        boolean isTrans = false;
        float transX = 1f;
        if (disL <= animatedValue && animatedValue <= disA) { //离开圆框,至到达下一个圆框
            isTrans = true;
            //我们设置2个圆框距离为mWidth / 2f
            transX = mWidth / 2f * range0Until1(disL, disA);  //[0,mWidth / 2f]
        }
        if (disA < animatedValue && animatedValue <= 1) {//到达下一个圆
            isTrans = true;
            transX = mWidth / 2;
        }

        if (isTrans) {
            canvas.translate(transX, 0);
        }

这里写图片描述


至此贝塞尔圆球进入右侧圆框的效果已经实现,那么如果圆球要从右侧圆框进入左侧圆框呢?
【题外话:写完上面这个效果已经是月黑风高的时候了,脑神经即将进入假死状态,我心想,虽然复杂了点,但是应该还是可以做的出来的,脑袋运行的速度根本跟不上敲代码的速度。根据位移方向的判断从而设定lRadio和rRadio。有点自信回头的赶脚。。。休息了一觉第二天醒来天啊噜,为什么不用Matrix,只要用path.transform(matrix),就可以做到镜像path,所以适当的休息有助于提升效率。】
这里写图片描述

        matrix_bounceL = new Matrix();
        matrix_bounceL.preScale(-1, 1);

        mPath.transform(matrix_bounceL);




4.Attach2ViewPager

关联ViewPager总共有2个要点

  • ViewPager的滑动监听,onPageScrolled。
    根据positionOffset和position,获取我们所要的当前位置/下一个位置/移动方向。
  • 手动选择ViewPager,即手指点击非当前圆框。

4.1 onPageScrolled

首先我们来了解一下onPageScrolled这个方法中2个我们要用到的参数

  • position : 当前cur位置,如果当前是1,手指按住右滑(vPage向左滑动)那就立马变为0。但如果当前是1,手指按住要左滑至下一个位置才为2
  • positionOffset : [0,1) ,到达下一个pos就置为0

这里写图片描述这里写图片描述

我们功能需求分析一下:

  1. 获取正确的当前位置curPos
  2. 获取正确的贝塞尔球进入的下一个位置nextPos
  3. 获取正确的贝塞尔球运动方向
  4. 配置正确的animatedValue

之前我们用ValueAnimator来模拟运动状态,现在我们可以使用positionOffset关联到ViewPager

        animatedValue = positionOffset;

        direction = ((position + positionOffset) - curPos > 0);  //运动方向。 true为右边(手往左滑动)
        nextPos = direction ? curPos + 1 : curPos - 1;  //右 +1   左 -1

        if (!direction)   //如果是向左
            animatedValue = 1 - animatedValue;  //让 animatedValue 不管是左滑还是右滑,都从[0,1)开始计算

        if (positionOffset == 0) { 
            curPos = position;
            nextPos = position;
        }

以上代码还需动手调试,看看log才能更明白的领悟。

这里写图片描述
从上面的gif可以发现如果缓慢的滑动,pos的位置正确的,但是如果快速滑动,就会发现问题 : [例如0快速滑动到2,贝塞尔圆球会从0滑动到1,再从0滑动到2],打了Log之后我们才发现原来快速滑动的时候,positionOffset到达下一个pos不会置为0!!发现问题后就好解决了。我们加上这一段代码就可以解决该问题。(快速滑动可能存在或多或少的问题,我也是花了些时间去测试的。)
这里写图片描述

        //快速滑动的时候,positionOffset有可能不会置于0
        if (direction && position + positionOffset > nextPos) {  //向右,而且
            curPos = position;
            nextPos = position + 1;
        } else if (!direction && position + positionOffset < nextPos) {
            curPos = position;
            nextPos = position - 1;
        }



onDraw
我们先要获得每个圆框的圆心x轴坐标

    private float[] bezPos; //记录每一个圆心x轴的位置

    bezPos = new float[default_round_count];  //根据圆框个数
    for (int i = 0; i < default_round_count; i++) {
    bezPos[i] = mWidth / (default_round_count + 1) * (i + 1);
    }

假设我们的default_round_count 即圆框个数为4,那么我们就要分成 4+1 份,再综合上述的求圆心代码,应该会更清晰一点。
这里写图片描述


根据curPos和nextPos绘制贝塞尔圆球,po出onDraw代码

        canvas.translate(0, mHeight / 2);

        mBezPath.reset();
        for (int i = 0; i < default_round_count; i++) {
            canvas.drawCircle(bezPos[i], 0, mRadius - 2, mRoundStrokePaint);   //绘制圆框
        }
        if (animatedValue == 1) {
            canvas.drawCircle(bezPos[nextPos], 0, mRadius, mBezPaint);
            return;
        }

        canvas.translate(bezPos[curPos], 0); //根据curPos,移动到当前圆框位置

        if (0 < animatedValue && animatedValue <= disL) {
            rRadio = 1f + animatedValue * 2;                         //  [1,2]
            lRadio = 1f;
            tbRadio = 1f;
        }
        if (disL < animatedValue && animatedValue <= disM) {
            rRadio = 2 - range0Until1(disL, disM) * 0.5f;          //  [2,1.5]
            lRadio = 1 + range0Until1(disL, disM) * 0.5f;          // [1,1.5]
            tbRadio = 1 - range0Until1(disL, disM) / 3;           // [1 , 2/3]
        }
        if (disM < animatedValue && animatedValue <= disA) {
            rRadio = 1.5f - range0Until1(disM, disA) * 0.5f;     //  [1.5,1]
            lRadio = 1.5f - range0Until1(disM, disA) * (1.5f - boundRadio);      //反弹效果,进场 内弹boundRadio
            tbRadio = (range0Until1(disM, disA) + 2) / 3;        // [ 2/3,1]
        }
        if (disA < animatedValue && animatedValue <= 1f) {
            rRadio = 1;
            tbRadio = 1;
            lRadio = boundRadio + range0Until1(disA, 1) * (1 - boundRadio);     //反弹效果,饱和
        }
        if (animatedValue == 1 || animatedValue == 0) {  //防止极其粗暴的滑动
            rRadio = 1f;
            lRadio = 1f;
            tbRadio = 1f;
        }

        boolean isTrans = false;  //根据nextPos和curPos求出位移距离
        float transX = (nextPos - curPos) * (mWidth / (default_round_count + 1));
        if (disL <= animatedValue && animatedValue <= disA) {
            isTrans = true;
            transX = transX * (animatedValue - disL) / (disA - disL);
        }
        if (disA < animatedValue && animatedValue <= 1) {
            isTrans = true;
        }
        if (isTrans) {
            canvas.translate(transX, 0);
        }

        mBezPath.moveTo(p0.x, p0.y * tbRadio);
        mBezPath.cubicTo(p1.x, p1.y * tbRadio, p2.x * rRadio, p2.y, p3.x * rRadio, p3.y);
        mBezPath.cubicTo(p4.x * rRadio, p4.y, p5.x, p5.y * tbRadio, p6.x, p6.y * tbRadio);
        mBezPath.cubicTo(p7.x, p7.y * tbRadio, p8.x * lRadio, p8.y, p9.x * lRadio, p9.y);
        mBezPath.cubicTo(p10.x * lRadio, p10.y, p11.x, p11.y * tbRadio, p0.x, p0.y * tbRadio);
        mBezPath.close();

        if (!direction) {
            mBezPath.transform(matrix_bounceL);
        }
        canvas.drawPath(mBezPath, mBezPaint);

        if (isTrans) {
            canvas.save();
        }


4.2 点击圆框,设置ViewPager的curItem

我们需要判断是否点击到了圆框上,和点击了具体哪个圆框。
onPageScrolled方法的时候不进行处理,而是通过ValueAnimator来模拟数值。从而绘制贝塞尔圆球效果。

        private float[] xPivotPos;  //根据圆心x轴+mRadius,划分成不同的区域 ,主要为了判断触摸x轴的位置
        xPivotPos = new float[default_round_count];
        for (int i = 0; i < default_round_count; i++) {
            xPivotPos[i] = mWidth / (default_round_count + 1) * (i + 1) + mRadius;
        }

这里写图片描述
针对x轴 : 我的做法是用一个数组xPivotPos 存储每个圆框最边缘的位置,即圆心+mRadius,然后我们触摸的时候,就可以找到当前触摸touchPos是属于哪个(圆框+mRadius)范围内。只要x >=bezPos[touchPos]-mRadius,就可以清楚的知道是否触摸到了该区域的圆框范围。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                float x = event.getX();
                float y = event.getY();

                if (y <= mHeight / 2 + mRadius && y >= mHeight / 2 - mRadius && !isAniming) {  //先判断y,如果y点击是在圆y轴的范围
                    int pos = -Arrays.binarySearch(xPivotPos, x) - 1;
                    if (pos >= 0 && pos < default_round_count && x + mRadius >= bezPos[pos]) {
                        nextPos = pos;
                        if (mViewPage != null && curPos != nextPos) {
                            mViewPage.setCurrentItem(pos);
                            isAniming = true;
                            direction = (curPos < pos);
                            startAnimator();  //我们通过ValueAnimator来模拟具体的值,不使用ViewPager的onPageScrolled方法。
                        }
                    }
                    return true;
                }
                break;
        }
        return super.onTouchEvent(event);
    }

至此我们BezierRoundView的用法和绘制方法已经讲解完了,下面来看一下ViewPager是怎么实现切换效果的。

实现ViewPager切换效果

参考【github.com/rubensousa/…

setClipToPadding

这里写图片描述这里写图片描述
【灵魂画家】
上图针对的是ViewPager设置Padding之后,

setClipToPadding 设置true,false不同的区别。 
左图是正常情况下默认 setClipToPadding(true) 的显示情况,设置Padding之后,手机屏幕上只显示width-PaddingLeft - PaddingRight。 


而如果设置 setClipToPadding(false) 情况,表示不裁剪Padding,这时候我们就可以看到左右的ViewPager,相当于原本两边的Padding透明度为1,而设置false之后透明度为0。


setMaxCardElevation

CardPagerAdapter是我们继承PagerAdapter的类,adapter里的布局是cardView

<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardView"
    app:cardCornerRadius="10dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:cardPreventCornerOverlap="true"
    app:cardUseCompatPadding="true">
    <!--cardUseCompatPadding 设置阴影之后自动缩小布局大小-->
    <ImageView
        android:id="@+id/item_iv"
        android:scaleType="fitXY"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.v7.widget.CardView>

先来了解一下cardView setCardElevation(float)方法。【针对CardViewApi21】

        if (!cardView.getUseCompatPadding()) {
            cardView.setShadowPadding(0, 0, 0, 0);
            return;
        }
        float elevation = getMaxElevation(cardView);
        final float radius = getRadius(cardView);
        int hPadding = (int) Math.ceil(RoundRectDrawableWithShadow
                .calculateHorizontalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
        int vPadding = (int) Math.ceil(RoundRectDrawableWithShadow
                .calculateVerticalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
        cardView.setShadowPadding(hPadding, vPadding, hPadding, vPadding);
    static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
            boolean addPaddingForCorners) {
        if (addPaddingForCorners) {
            return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
        } else {
            return maxShadowSize * SHADOW_MULTIPLIER;
        }
    }

    static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
            boolean addPaddingForCorners) {
        if (addPaddingForCorners) {
            return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
        } else {
            return maxShadowSize;
        }
    }

下面看一下效果测试。

ViewPager效果测试

我们来看一下ViewPager左右设置Padding为mWidth / 10的效果

        viewPager.setPadding(mWidth / 10, 0, mWidth / 10, 0);
        viewPager.setClipToPadding(false);

这里写图片描述

再来看一下CardPagerAdapter设置MaxElevationFactor为mWidth / 10的效果【adapter.xml的cardCornerRadius不设值,cardUseCompatPadding一定要设置true!!】

        int maxFactor = mWidth / 10;
        cardAdapter.setMaxElevationFactor(maxFactor);

这里写图片描述

具体我也不赘述了,看图应该能分析出两者的不同。
所以现在综上所述,制定一个需求

  • 不管是设置padding还是Elevation都要保持图片的宽高比例。

也就是说当我们知道图片的宽高比例之后,代码里面我们要动态的去调整和设置并保持这个宽高比例。 


【这边有个坑就是设置setMaxElevation它的宽高比是不可抗的,所以我们只能在setPadding的时候,去调节这个比例】 


【setMaxElevation
宽的Padding为maxFactor + 0.3*CornerRadius 【0.3≈≈ (1 - COS_45)】
高的Padding为maxFactor*1.5f + 0.3*CornerRadius】

这里写图片描述

但是!

(鸡生的)
如果我们在setMaxElevation的情况下,在去设置padding,那么如何保证我们的宽高比?具体请看如下代码分析。【可以通过去掉adapter.xml 里ImagerView 的android:scaleType=”fitXY”属性测试一下宽高比例是否调试正确

        //已知图片的宽为1920,高1080.
        int mWidth = getWindowManager().getDefaultDisplay().getWidth();
        float heightRatio = 0.565f;  //高是宽的 0.565 ,根据图片比例

        CardPagerAdapter cardAdapter = new CardPagerAdapter(getApplicationContext());
        cardAdapter.addImgUrlList(imgList);//添加加载的图片集合


        //设置阴影大小,即vPage  左右两个图片相距边框  maxFactor + 0.3*CornerRadius   *2
        //设置阴影大小,即vPage 上下图片相距边框  maxFactor*1.5f + 0.3*CornerRadius
        int maxFactor = mWidth / 25;
        cardAdapter.setMaxElevationFactor(maxFactor);

        int mWidthPading = mWidth / 8;
        //因为我们adapter里的cardView CornerRadius已经写死为10dp,所以0.3*CornerRadius=3
        //设置Elevation之后,控件宽度要减去 (maxFactor + dp2px(3)) * heightRatio
        //heightMore 设置Elevation之后,控件高度 比  控件宽度* heightRatio  多出的部分
        float heightMore = (1.5f * maxFactor + dp2px(3)) - (maxFactor + dp2px(3)) * heightRatio;
        int mHeightPading = (int) (mWidthPading * heightRatio - heightMore);

        BezierViewPager viewPager = (BezierViewPager) findViewById(R.id.view_page);
        viewPager.setLayoutParams(new RelativeLayout.LayoutParams(mWidth, (int) (mWidth * heightRatio)));
        viewPager.setPadding(mWidthPading, mHeightPading, mWidthPading, mHeightPading);
        viewPager.setClipToPadding(false);
        viewPager.setAdapter(cardAdapter);


showTransformer

改方法是设置ViewPager移动的时候,cardView放大效果和Elevation阴影效果,具体过程可以自行在ShadowTransformer查看,实现过程上文基本也有覆盖。

总结

零零碎碎也捣鼓了一阵子的自定义View,我在想既然迈出这一步了,就得做好它。

人生总是要有信仰,有梦想才能一直前行,哪怕走的再慢,也是在前行。

如果这篇文章写的还凑合或者勾引起了你的斗志的话,欢迎点个star
github.com/qdxxxx/Bezi…