preface

I saw this effect the other day











The effect is really cool, it feels biu kick, let’s just do it.


Modified enhanced version, improved round into the frame of the tail effect, the most important point is


Enhanced ViewPager toggle effects and card shadows]





Integrated mode [Hand party welfare]

Github address: github.com/qdxxxx/Bezi… Thank you so much for being a star. Title party is generally: turn crazy, project integration this cool dazzle animation as long as 3 steps!

  • Step 1. Add the JitPack repository to your build file Step 2
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io'}}}Copy the code

dependencies {
    compile 'com. Making. QDXXXX: BezierViewPager: v1.0.5'
}Copy the code


  • XML layout code
    <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"
     />Copy the code


  • Integrate code into the Activity
CardPagerAdapter cardAdapter = new CardPagerAdapter(getApplicationContext()); cardAdapter.addImgUrlList(imgList); List BezierViewPager viewPager = (BezierViewPager) findViewById(r.i.view_page); viewPager.setAdapter(cardAdapter); BezierRoundView bezRound = (BezierRoundView) findViewById(R.id.bezRound); bezRound.attach2ViewPage(viewPager);Copy the code




Methods and properties are introduced

  • BezierRoundView
name format Chinese interpretation
color_bez color Bezier ball color
color_touch color Touch feedback
color_stroke color The color of the round frame
time_animator integer Animation time
round_count integer The number of circles, that is, adapter.getCount
radius dimension Radius of bezier sphere, radius of frame is radius-2.
attach2ViewPage BezierViewPager Bind the specified ViewPager(handles touch events while sliding)

Round_count is automatically set


  • BezierViewPager[extends ViewPager]
name format Chinese interpretation
showTransformer float ViewPager slides to the zoom scale of the currently displayed page


  • CardPagerAdapter[extends PagerAdapter]
name format Chinese interpretation
addImgUrlList List A list containing the address of the image
setOnCardItemClickListener OnCardItemClickListener Current ViewPager click event

Return CurPosition
setMaxElevationFactor integer The largest Elevation of the CardView in Adapter






Achieve anatomical

1. Start by implementing bezier circles

[I suggest reading this article first]

First we need to render P0, then cubicTo P1, P2, P3, and then cubicTo P4,p5.p6… Forgive me for drawing circles in such a crude way…

    private PointF p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11;
Copy the code

p0 = new PointF(0, -mRadius); //mRadius radius p6 = new PointF(0, mRadius); p1 = new PointF(mRadius * bezFactor, -mRadius); / / bezFactor is 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);Copy the code

Draw the path again

        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();Copy the code



A ri circle Ben guo circle Qi is lifelike.


We try to see how the circle changes by sliding the finger around the x coordinates of P2, P3, and P4

@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;
    }Copy the code


2. Anatomical renderings







First of all, we do not consider the rebound effect, there are three states of the circle change

  1. Bezier circle does not leave the circle, p2,3,4 x coordinates change from r to 2r.
  2. Bezier circle leaves the circle and reaches the center position [p2,3,4 x coordinates change from 2r to 1.5r],[p8,9,10 x coordinates change from r to 1.5r]
  3. Bezier circles move from the center to the next circle. [p2,3,4, 8,9,10 x coordinates changed from 1.5r to r]

As usual, we use ValueAnimator to simulate the value of [0,1]. Because ViewPager onPageScrolled listener positionOffset is [0,1) changed, similar.

Surprised! The next few pieces of code! The man saw silence, the woman saw tears.

Private ValueAnimator animatorStart; private TimeInterpolator timeInterpolator = new DecelerateInterpolator(); privatefloatanimatedValue; // the value of [0,1] public voidstartAnimator() {
        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(); }}Copy the code

    private floatrRadio=1; //P2,3,4 x multiples privatefloatlRadio=1; / / P8, 9, 10 multiple privatefloattbRadio=1; // Y scale privatefloatDisL = 0.5 f; // The threshold for leaving the circle privatefloatDisM = 0.8 f; // Maximum threshold privatefloatDisA = 0.9 f; // Reaches the threshold of the next circleCopy the code

        if(0 < animatedValue && animatedValue <= disL) {rRadio = 1f + animatedValue * 2; / /} [1, 2]if(disL < animatedValue && animatedValue <= disM) {rRadio = 2 - range0Until1(disL, disM) * 0.5f; // lRadio = 1 + range0Until1(disL, disM) * 0.1f; / /,1.5 [1]}if(disM < animatedValue && animatedValue <= disA) {rRadio = 1.5f-range0untiL1 (disM, disA) * 0.5f; // [1.5,1] lRadio = 1.5 f-range0untiL1 (disM, disA) * 0.5f; / /} [1.5, 1]Copy the code

/** * convert the value field to [0,1] ** @param minValue is greater than or equal to * @param maxValue is less than or equal to * @returnReturns the value */ private corresponding to [0,1], based on the current animatedValuefloat range0Until1(float minValue, float maxValue) {
        return (animatedValue - minValue) / (maxValue - minValue);
    }Copy the code

Please forgive me again for drawing circles in such a crude and simple way…

        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();
Copy the code

With the above code sorted out, a clever Bezier circle is on the verge of being drawn. We add the change in the Y-axis between leaving the circle and reaching the next circle, [p,5,6,7, 1,0,11], and the result is as follows.

3. Simulation effect

Now that we have represented the motion of the Bezier circle, with some effects [displacement/bounce/flip], we can animate the Bezier circle from one frame to the next. On top of that, we add the rebound effect

        if(0 < animatedValue && animatedValue <= disL) {rRadio = 1f + animatedValue * 2; / /} [1, 2]if(disL < animatedValue && animatedValue <= disM) {rRadio = 2 - range0Until1(disL, disM) * 0.5f; // lRadio = 1 + range0Until1(disL, disM) * 0.1f; // [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.5 f-range0until1 (disM, disA) * (1.5 f-boundradio); BoundRadio = (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); // Rebound effect, saturation}Copy the code

Plus the displacement effect. At first I thought, bezier circles are constantly changing shape and moving position. It’s a lot of trouble. It was then decomposed into a variable state + continuous displacement effect.

        boolean isTrans = false;
        float transX = 1f;
        if(disL <= animatedValue && animatedValue <= disA) {// Leave the circle until the next circle is reachedtrue; TransX = mWidth / 2f * range0Until1(disL, disA); //[0,mWidth / 2f] }if(disA < animatedValue && animatedValue <= 1) {// Reach the next circle isTrans =true;
            transX = mWidth / 2;
        }

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







Now that the bezier ball enters the right circle, what if the ball enters the left circle from the right circle?

【 Digression 】 Finish this effect is already midnight, cranial nerve is about to enter a state of suspended animation, I think, although a little complicated, but should or can do, head run faster with knock code. According to the displacement direction judgment to set lRadio and rRadio. A little confidence back at… After a sleep, I woke up the next day. Oh my god, why not use Matrix? Just use path.transform(Matrix) to mirror path, so proper rest helps improve efficiency.

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

        mPath.transform(matrix_bounceL);
Copy the code






4.Attach2ViewPager

There are two main points in associating ViewPager

  • ViewPager slide listener, onPageScrolled. Get the current position/next position/move direction we want based on positionOffset and position.
  • Manually select the ViewPager, that is, finger click on the non-current circle.

4.1 onPageScrolled

First let’s take a look at the two parameters we need to use in the onPageScrolled method

  • Position: The current cur position. If the current is 1, hold your finger and swipe right (vPage swipe left) and it immediately changes to 0. But if it’s currently 1, finger hold is 2 until you slide left to the next position
  • PositionOffset: [0,1), set to 0 at the next POS

Let’s analyze the functional requirements:

  1. Gets the correct current location curPos
  2. Get the correct Bezier ball into the next position nextPos
  3. Gets the correct direction of motion of the Bezier ball
  4. Configure the animatedValue correctly

Previously we used ValueAnimator to simulate the motion state. Now we can associate the ViewPager with positionOffset

animatedValue = positionOffset; direction = ((position + positionOffset) - curPos > 0); // Direction of motion.trueNextPos = direction? curPos + 1 : curPos - 1; // Right +1 left -1if(! AnimatedValue = 1 - animatedValue; // let animatedValue start from [0,1), whether left or rightif (positionOffset == 0) { 
            curPos = position;
            nextPos = position;
        }
Copy the code

The above code also needs to be debugged, look at the log to understand more clearly.



From the GIF above, you can see that pos is in the correct position if you slide slowly, but if you slide quickly, you will find a problem: [for example, if 0 slides quickly to 2, the Bezier sphere will slide from 0 to 1 and then from 0 to 2]. After logging, we found that the positionOffset will not be set to 0 when it reaches the next POS. The problem will be solved when it’s discovered. We can solve this problem by adding this piece of code. (Quick swipes can be more or less problematic, and I spent some time testing them.)

// The positionOffset may not be set to 0 when sliding quicklyif(direction && position + positionOffset > nextPos) {// Go to the right, and curPos = position; nextPos = position + 1; }else if(! direction && position + positionOffset < nextPos) { curPos = position; nextPos = position - 1; }Copy the code

OnDraw We first need to get the X-axis coordinates of the center of each circle

    private float[] bezPos; // Record the X-axis position of each center bezPos = newfloat[default_round_count]; // Based on the number of circlesfor (int i = 0; i < default_round_count; i++) {
    bezPos[i] = mWidth / (default_round_count + 1) * (i + 1);
    }
Copy the code

Assuming that our default_round_count is 4, then we’re going to split it into 4+1 pieces, and it should be a little clearer if we use the code above to find the center of the circle.







Draw bezier spheres according to curPos and nextPos, Po out onDraw code

        canvas.translate(0, mHeight / 2);

        mBezPath.reset();
        for(int i = 0; i < default_round_count; i++) { canvas.drawCircle(bezPos[i], 0, mRadius - 2, mRoundStrokePaint); // Draw a circle}if (animatedValue == 1) {
            canvas.drawCircle(bezPos[nextPos], 0, mRadius, mBezPaint);
            return; } canvas.translate(bezPos[curPos], 0); // Move to the current box position according to curPosif (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; // lRadio = 1 + range0Until1(disL, disM) * 0.1f; // [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.5 f-range0until1 (disM, disA) * (1.5 f-boundradio); BoundRadio = (range0Until1(disM, disA) + 2) / 3; / / / 2/3, 1}if(disA < animatedValue && animatedValue <= 1f) { rRadio = 1; tbRadio = 1; lRadio = boundRadio + range0Until1(disA, 1) * (1 - boundRadio); // Rebound effect, saturation}if(animatedValue = = 1 | | animatedValue = = 0) {/ / prevent extremely brutal sliding rRadio = 1 f; 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();
        }

Copy the code


4.2 Click the box and set the curItem of ViewPager

We need to determine whether we clicked on the circle, and which circle we clicked on. Instead of processing the onPageScrolled method, the value is simulated by ValueAnimator. To draw the Bezier orb effect.

        private float[] xPivotPos; // According to the center of the circle x axis +mRadius, it is divided into different regions, mainly to determine the position of touching the X axis xPivotPos = newfloat[default_round_count];
        for (int i = 0; i < default_round_count; i++) {
            xPivotPos[i] = mWidth / (default_round_count + 1) * (i + 1) + mRadius;
        }Copy the code



For the X-axis: MY approach is to use an array xPivotPos to store the position at the very edge of each circle, the center of the circle +mRadius. Then, when we touch, we can find which (circle +mRadius) range the current touch touchPos belongs to. As long as x >=bezPos[touchPos] -mradius, we can clearly know whether we have touched the circle of the area.



@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 && ! Int pos = -Array. 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(); // We use ValueAnimator to simulate specific values, not ViewPager's onPageScrolled method. }}return true;
                }
                break;
        }
        return super.onTouchEvent(event);
    }Copy the code

Now that we’ve covered the use of BezierRoundView and its drawing methods, let’s look at how ViewPager implements the switch effect.

Implement the ViewPager switch effect

See github.com/rubensousa/…

setClipToPadding



【 Soul Painter 】

The image above is for ViewPager after setting the Padding,

setClipToPadding Set true, false different differences.

The default value of setClipToPadding(True) is left. After the Padding is set, only width-paddingleft-paddingright is displayed on the phone screen.


If setClipToPadding(False) is set, the Padding is not clipped, so we can see the left and right ViewPager, which is equal to the Padding opacity of 1 and 0.






setMaxCardElevation

Cardpage Adapter is the class that we inherit from Page Adapter, and the layout inside the Adapter is 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"> <! --> <ImageView Android :id="@+id/item_iv"
        android:scaleType="fitXY"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.v7.widget.CardView>Copy the code

So let’s take a look at the cardView set card creation (float) method. 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);Copy the code

    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 {
            returnmaxShadowSize; }}Copy the code

Let’s take a look at the effect test.

ViewPager effect test

Let’s see what happens when we set the Padding to mWidth / 10 on the left and right of ViewPager

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

Adapter. XML cardCornerRadius is set to true. CardUseCompatPadding must be set to true. 】

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

I won’t go into details, but if you look at the picture you can see the difference. So now to sum up, make a requirement

  • Keep the image’s width to height ratio when setting the padding or Elevation.

That is to say, when we know the width to height ratio of the image, we need to adjust and set the code dynamically and keep the width to height ratio.

There is a pit here that sets the setMaxElevation, which has a fixed aspect to height ratio, so we can only adjust the ratio when setPadding is used.

SetMaxElevation Wide Padding is maxFactor + 0.3*CornerRadius 0.3≈ (1-cos_45) High Padding is maxFactor*1.5f + 0.3 * CornerRadius 】

But!

(Chicken)

If we were in the
setMaxElevationAfter setting the padding, how do we ensure the width ratio? See the code analysis below for details. You can test the width-to-height ratio by removing the Android :scaleType= “fitXY” attribute of ImagerView from adapter.xml


Int mWidth = getWindowManager().getDefaultDisplay().getwidth ();floatHeightRatio = 0.565 f; CardPagerAdapter cardAdapter = new CardPagerAdapter(getApplicationContext()); cardAdapter.addImgUrlList(imgList); MaxFactor + 0.3*CornerRadius *2 // Set the shadow size, MaxFactor *1.5f + 0.3*CornerRadius int maxFactor = mWidth / 25; cardAdapter.setMaxElevationFactor(maxFactor); int mWidthPading = mWidth / 8; CardView CornerRadius= "10dp" and "CornerRadius=" 3" By subtracting (maxFactor + dp2px(3)) * heightRatio //heightMore The height of the control is highlighted by the * heightRatio of the control width when the Elevation is setfloatHeightMore = (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);Copy the code


showTransformer

The modification method is to set the cardView magnification effect and Elevation shadow effect when ViewPager moves. The specific process can be viewed in ShadowTransformer by yourself. The implementation process is also covered above.

conclusion

I’ve been tinkering with custom views for a while, and I’m thinking now that I’ve taken this step, I need to do it well.

Life is always to have faith, have a dream can always move forward, even if you go slowly, but also on the move.

If this article is still acceptable or seduce up your fight, welcome to click on the star github.com/qdxxxx/Bezi…