Android进阶之自定义View实战贝塞尔曲线应用

Posted kakacxicm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android进阶之自定义View实战贝塞尔曲线应用相关的知识,希望对你有一定的参考价值。

android进阶之自定义View实战(三)贝塞尔曲线应用

一、引言

在自定义View中,常常看到这样一些非常规的UI效果,如水滴、心型、水波、仿真书页翻动、弹射床等效果,这里面都包含一个重要的要素:贝塞尔曲线(Bézier curve)。
贝塞尔曲线依据n(n>=3)个点位置任意的点坐标绘制出的一条光滑曲线。贝塞尔曲线的有趣之处更在于它的“皮筋效应”。常用的有二阶和三阶曲线。下面分别是二阶和三阶曲线的实现效果和方程。
二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数,它们分别称为起点、锚点(控制点)、终点。B(t)追踪:

方程:

三次贝塞尔曲线由P0、P1、P2、P3四个点确定。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。B(t)追踪:

方程:

关于贝塞尔曲线的数学描述,大家有个直观的感觉就好。关键在于如何灵活的应用它。

二、入门API

Android提供了二阶与三阶贝塞尔曲线的绘制方法:Path的quadTo和cubicTo方法。后者和前者的区别仅仅是多了一个锚点而已。
二次贝塞尔曲线绘制代码

mPath.moveTo(100, 500);
mPath.quadTo(300, 100, 600, 500);
canvas.drawPath(mPath, mPaint);

效果

三次贝塞尔曲线绘制代码:

mPath.moveTo(100, 500);
mPath.cubicTo(100, 500, 300, 100, 600, 500);

效果:

动态的绘制贝尔曲线的主要工作就是确定各个点的位置,如弹射床的效果只需要改变二次曲线的锚点即可。一些相对复杂的效果则需要多个点同时计算,如水滴效果、QQ上的滑动删除小气泡的粘性效果等等。曲线动态变化逻辑的实质是一些平面几何的运算,可能涉及到圆的切线、三角函数等基础知识。

三、范例分析

前段时间无意间在github上发现了一个有意思的ViewPagerIndicator:SpringIndicator

这里就运用到贝塞尔曲线(ps:真是佩服这设计师的脑洞..)。下面我就按照自己的思路简单山寨一下这个例子中的Indicator页面切换时的效果。
老套路,先分解绘制元素:
1.变化时左边的圆和右边的圆,它这里做了变换过程中的圆半径的变化,左边的圆半径由大变小,右边的圆半径又小变大,因为贝塞尔曲线的端点均在圆上,所以半径的变化会影响两条贝塞尔曲线的4个端点;
2.上下X轴对称的两条二阶贝塞尔曲线,起点和终点均与两个圆位置相关,X坐标分别为俩圆的圆心X,Y坐标为(cy+lr)、(cy-rr),其中cy为俩圆心坐标的y值,lr和rr分别为左右圆的半径;控制点的X坐标为两圆心坐标连线的中点,Y坐标做滑动的比率(0,1)—>圆半径(0,r)的线性映射。
3.变化开始时,左圆未进行平移,一段比率之后,才开始滑动,这里需要分段函数计算左圆的水平偏移;两圆的半径分别在阙值范围内做递增和递减的线性变化。
通过以上分析, 滑动过程中只要控制好两条贝塞尔曲线的6个点,实现这样看起来很酷的效果也并非难事儿!
下面是范例代码:

 package com.star.springindicatordemo;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by star on 16/6/28.
 */
public class SpringIndicator extends View

    private Paint mStaticCirclePaint;//静态圆形的画笔
    private Paint mDynamicBezierPaint;//贝塞尔曲线的画笔
    private Path mBezierPath;//贝塞尔曲线

    private final int MAXCIRCLERADIUS = 20;//最大圆半径
    private final int MINCIRCLERADIUS = 10;//最小圆半径

    private float mLeftCircleRadius;//左圆的半径
    private float mRightCircleRadius;//右边圆半径

    //贝塞尔原点
    private float MORIGINCENTERX = 20;
    private float MORIGINCENTERY = 20;

    private float mCircleGap = 20*8;

    private float mLeftX;//贝塞尔曲线的左边起点X
    private float mRightX;//贝塞尔曲线的右边边终X

    private float mLeftTopY;//贝塞尔曲线的左上起点y
    private float mLeftBottomY;//贝塞尔曲线的右上边终y
    private float mRightBottomY;//贝塞尔曲线的右上边终y
    private float mRightTopY;//贝塞尔曲线的右上上起点y

    private float mControlX;//贝塞尔曲线的控制点X
    private float mControlTopY;//贝塞尔曲线的控制点
    private float mControlBottomY;

    private float bezierFraction;//控制点在水平方向相对小圆半径偏移比例

    public SpringIndicator(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    public float getBezierFraction() 
        return bezierFraction;
    

    //动画测试,更新View逻辑
    public void setBezierFraction(float bezierFraction) 
        this.bezierFraction = bezierFraction;
        //更新右圆的位置
        mRightX = MORIGINCENTERX + bezierFraction*mCircleGap;
        //更新左圆的位置
        mLeftX = MORIGINCENTERX + leftCircleDelayTranslationXLogic(bezierFraction);

        //更新控制点
        mControlX = (mLeftX + mRightX)/2;
        mControlTopY = MORIGINCENTERY - MAXCIRCLERADIUS *(1-bezierFraction);
        mControlBottomY = MORIGINCENTERY + MAXCIRCLERADIUS *(1-bezierFraction);

        //更新俩圆半径
        mLeftCircleRadius = MAXCIRCLERADIUS + (MINCIRCLERADIUS - MAXCIRCLERADIUS)*bezierFraction;
        mRightCircleRadius = MINCIRCLERADIUS + (MAXCIRCLERADIUS - MINCIRCLERADIUS)*bezierFraction;

        //更新起止点
        mLeftTopY = MORIGINCENTERY - mLeftCircleRadius;
        mLeftBottomY = MORIGINCENTERY + mLeftCircleRadius;
        mRightTopY = MORIGINCENTERY - mRightCircleRadius;
        mRightBottomY = MORIGINCENTERY + mRightCircleRadius;


        invalidate();
    

    //左边的球延时移动
    private float leftCircleDelayTranslationXLogic(float elapsedFraction)
        if(elapsedFraction < 0.6f)
            return 0;
        
        float leftTranslatinX = ((2*elapsedFraction - 1)*mCircleGap);
        return leftTranslatinX;
    

    private void init()
        mStaticCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mStaticCirclePaint.setStyle(Paint.Style.FILL);

        mDynamicBezierPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDynamicBezierPaint.setColor(Color.BLUE);
        mDynamicBezierPaint.setStyle(Paint.Style.FILL);

        mBezierPath = new Path();

        //半径
        mLeftCircleRadius = MAXCIRCLERADIUS;
        mRightCircleRadius = MINCIRCLERADIUS;

        //初始化贝塞尔曲线的6个点
        //4个起止点
        mLeftX = MORIGINCENTERX;
        mRightX = MORIGINCENTERX;

        mLeftTopY = MORIGINCENTERY - mLeftCircleRadius;
        mLeftBottomY = MORIGINCENTERY + mLeftCircleRadius;
        mRightTopY = MORIGINCENTERY - mRightCircleRadius;
        mRightBottomY = MORIGINCENTERY + mRightCircleRadius;

        //控制点
        mControlX = (mLeftX + mRightX)/2;
        mControlTopY = MORIGINCENTERY - MAXCIRCLERADIUS *(1-bezierFraction);
        mControlBottomY = MORIGINCENTERY + MAXCIRCLERADIUS *(1-bezierFraction);
    

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);
        int resultWidth = wSize;
        int resultHeight = hSize;
        //lp = wrapcontent时 指定默认值
        if(wMode == MeasureSpec.AT_MOST)
            resultWidth = (int) (MORIGINCENTERX + MAXCIRCLERADIUS *2+mCircleGap);
        
        if(hMode == MeasureSpec.AT_MOST)
            resultHeight = (int) (MAXCIRCLERADIUS + MORIGINCENTERY);
        
        setMeasuredDimension(resultWidth, resultHeight);
    

    @Override
    protected void onDraw(Canvas canvas) 
        mStaticCirclePaint.setColor(Color.BLUE);
        //左右的圆
        canvas.drawCircle(mLeftX, MORIGINCENTERY, mLeftCircleRadius, mStaticCirclePaint);
        canvas.drawCircle(mRightX, MORIGINCENTERY, mRightCircleRadius, mStaticCirclePaint);
        mBezierPath.reset();

        //上下两条控制点
        mBezierPath.moveTo(mLeftX, mLeftTopY);
        mBezierPath.quadTo(mControlX, mControlTopY, mRightX, mRightTopY);

        mBezierPath.lineTo(mRightX, mRightBottomY);
        mBezierPath.quadTo(mControlX,mControlBottomY, mLeftX, mLeftBottomY);
        canvas.drawPath(mBezierPath, mDynamicBezierPaint);
    

    /**
     * 启动变幻测试
     */
    public void start()
        ObjectAnimator animator = ObjectAnimator.ofFloat(this, "bezierFraction", 0, 1.0f);
        animator.setDuration(2000);
        animator.start();

    

说明:
1.init()方法对圆半径及贝塞尔曲线的六个点做初试化,开始右圆与左圆位置重合,半径一大一小.
2.bezierFraction是模拟Viewpage的滑动offset,结合属性动画,模拟页面切换;start方法启动变幻,动画启动后,setBezierFraction会在动画更新时调用,更新绘制参量,做重绘操作.这里涉及的绘制参量有:两圆的半径变化影响贝塞尔曲线4个端点的Y坐标,俩圆心坐标的变化影响曲线2个控制点的X坐标;
3.leftCircleDelayTranslationXLogic是采用分段函数处理左圆的水平移动逻辑:0-0.5时,不移动;0.5-1.0内,做0-mCircleGap的线性映射.
DemoActivity:

package com.star.springindicatordemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity 
    private SpringIndicator mVpi;
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mVpi = (SpringIndicator) findViewById(R.id.spring_vpi);
        findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                mVpi.start();
            
        );
    

总之,不要被贝塞尔曲线这中高大上的名词唬住,控制了那几个”痛点”,你想怎么玩就怎么玩!

以上是关于Android进阶之自定义View实战贝塞尔曲线应用的主要内容,如果未能解决你的问题,请参考以下文章

Android进阶之自定义View实战九宫格手势解锁实现

Android进阶之自定义View实战九宫格手势解锁实现

Android进阶之自定义View实战九宫格手势解锁实现

Android进阶之自定义View实战仿iOS UISwitch控件实现

Android进阶之自定义View实战仿iOS UISwitch控件实现

安卓自定义View进阶 - 贝塞尔曲线