Android 贝塞尔曲线实践 —— 旋转的七色花朵

241 阅读3分钟

一、关于贝塞尔曲线

在工业设计方面贝塞尔曲线有很多用途,比如字体设计,模型设计,任务构型等方方面都有使用,因此,对于绘制感兴趣的开发者,掌握贝塞尔曲线是非常必要的。

在 Android 中,贝塞尔曲线结合 Path 类可以实现更复杂的图形,这里我们给一个案例,来实现一种旋转的花朵。

二、自定义 View 的误区

今天我们自定义的 View 效果如下:

对于花朵而言,首先要构建花瓣,花瓣这里使用了三阶贝赛尔曲线,绘制更加精确,二阶贝赛尔曲线用来画树叶,也是不错的。

    private void buildLeaf(Canvas canvas){
        mPaint.setColor(0xff40835e);
        int width = getWidth();
        int height = getHeight();
        if(width==0 || height==0){
            return;
        }

        int centerX = width/2;
        int centerY = height/2;

        int Radius = Math.max(width,height)/2;

        Path path = new Path();

        path.moveTo(0,0);

       // float leftctrY = - (Radius*4)/5.0f;
        float leftctrY = - (Radius*7)/10.0f;
        float leftctrX = -(float) (Math.abs(leftctrY) * Math.tan(Math.toRadians(30)));
       // path.lineTo(leftctrX,leftctrY);

        int lastX = 0;
        int lastY = -Radius;

        float rightctrY = - (Radius*7)/10.0f;
        float rightctrX = (float) (Math.abs(rightctrY) * Math.tan(Math.toRadians(30)));

        path.quadTo(leftctrX,leftctrY,lastX,lastY);
        path.quadTo(rightctrX,rightctrY,0,0);

        path.close();
        path.setFillType(Path.FillType.WINDING);



        int restoreId = canvas.save();
        Paint.Style style = mPaint.getStyle();
       // mPaint.setStyle(Paint.Style.STROKE);
        canvas.translate(centerX,centerY);
        canvas.drawPath(path,mPaint);

        mPaint.setStyle(style);
        canvas.restoreToCount(restoreId);

    }

而我们需要的带有弧度的花瓣,因此二阶显然不行,花瓣的画法难度主要集中于三角函数的计算,此外还有第二个控制点的确定,第二个控制点与原点的举例实际上和离原点最远的边平行,此外过最远的点作垂线相交,否则可能产生如下效果。

   private void buildHeart(Canvas canvas){
        mPaint.setColor(0xffa7324a);
        int width = getWidth();
        int height = getHeight();
        if(width==0 || height==0){
            return;
        }

        int centerX = width/2;
        int centerY = height/2;

        int Radius = Math.max(width,height)/2;

        Path path = new Path();

        path.moveTo(0,0);

        // float leftctrY = - (Radius*4)/5.0f;
        float leftctrY = - (Radius*5)/10.0f;
        float leftctrX = -(float) (Math.abs(leftctrY) * Math.tan(Math.toRadians(60)));
        // path.lineTo(leftctrX,leftctrY);

        int lastX = 0;
        float lastY = -Radius * 8f/10;

        float rightctrY = - (Radius*5)/10.0f;
        float rightctrX = (float) (Math.abs(rightctrY) * Math.tan(Math.toRadians(60)));

        path.cubicTo(leftctrX,leftctrY,leftctrX,-Radius,lastX,lastY);
        path.cubicTo(rightctrX,-Radius,rightctrX,rightctrY,0,0);
        path.close();
        path.setFillType(Path.FillType.WINDING);


        int restoreId = canvas.save();
        Paint.Style style = mPaint.getStyle();
        canvas.translate(centerX,centerY);
        canvas.drawPath(path,mPaint);

        mPaint.setStyle(style);
        canvas.restoreToCount(restoreId);

    }

三、实现自定义 View 

旋转的七色花朵

3.1 颜色表定义

我们这里定义颜色表,目的是为每个花瓣都有不同的颜色

    private static int[] COLOR_TABLE = new int[]{
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random())
};

3.2 旋转

旋转流程我们使用动画来实现

   public void startRotate() {
        stopRotate();
        isPlaying = true;
        if (animator == null) {
            animator = ValueAnimator.ofFloat(0, 360);
            animator.setEvaluator(new TypeEvaluator<Float>() {
                @Override
                public Float evaluate(float fraction, Float startValue, Float endValue) {
                    return fraction * 360;
                }
            });
            animator.setRepeatCount(ValueAnimator.INFINITE);
            animator.setInterpolator(new LinearInterpolator());
            animator.setDuration(3000)
                    .setRepeatMode(ValueAnimator.RESTART);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    setDegree((Float) animation.getAnimatedValue());

                }
            });
        }
        animator.start();
    }

3.3 代码逻辑

下面是完整的实现逻辑

public class FlowerView extends View {

    private TextPaint mPaint;

    private int strokeWidth = 1;
    private int textSize = 12;

    private int minContentSize = 0;
    private Path mPath;
    private int mPetalNumbers = 7;
    private float degree;
    private int defaultPetalColor = 0xFFFF1493;
    private static int[] COLOR_TABLE = new int[]{
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
            argb((float) Math.random(), (float) Math.random(), (float) Math.random())
};

    private boolean isPlaying = false;
    private int[] colorSet = COLOR_TABLE;

    public FlowerView(Context context) {
        this(context, null);
    }

    public FlowerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setClickable(true);
        initPaint();
        minContentSize = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
        mPath = new Path();
    }

    public boolean isPlaying() {
        return isPlaying;
    }

    private void initPaint() {
        // 实例化画笔并打开抗锯齿
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setPathEffect(new CornerPathEffect(10)); //设置线条类型
        mPaint.setStrokeWidth(dip2px(strokeWidth));
        mPaint.setTextSize(dip2px((textSize)));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            width = (int) dip2px(210);
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            height = (int) dip2px(210);
        }
        setMeasuredDimension(width, height);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        final int width = getWidth();
        final int height = getHeight();

        if (width < minContentSize || height < minContentSize) return;
        int contentSize = Math.min(width, height);  //取最小边长,防止画出边界

        int centerX = width / 2;
        int centerY = height / 2;

        final int restoreId = canvas.save();
        canvas.translate(centerX, centerY);  //将坐标系移到中心
        mPaint.setStyle(Paint.Style.STROKE);
        drawFlower(canvas, contentSize);
        canvas.restoreToCount(restoreId);

    }

    public void setPetalNumber(int num, int[] colorSet) {
        this.mPetalNumbers = num;
        if(colorSet != null){
            this.colorSet = colorSet;
        }else{
            this.colorSet = COLOR_TABLE;
        }
        invalidate();
    }

    ValueAnimator animator = null;

    public void startRotate() {
        stopRotate();
        isPlaying = true;
        if (animator == null) {
            animator = ValueAnimator.ofFloat(0, 360);
            animator.setEvaluator(new TypeEvaluator<Float>() {
                @Override
                public Float evaluate(float fraction, Float startValue, Float endValue) {
                    return fraction * 360;
                }
            });
            animator.setRepeatCount(ValueAnimator.INFINITE);
            animator.setInterpolator(new LinearInterpolator());
            animator.setDuration(3000)
                    .setRepeatMode(ValueAnimator.RESTART);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    setDegree((Float) animation.getAnimatedValue());

                }
            });
        }
        animator.start();
    }


    public void stopRotate() {
        isPlaying = false;
        if (animator != null) {
            animator.cancel();
            animator = null;
        }
    }

    private void drawFlower(Canvas canvas, int contentSize) {
        int N = this.mPetalNumbers;
        for (int i = 0; i < N; i++) {
            drawFlowerPath(canvas, contentSize, N, i);
        }
    }

//花瓣算法

    private void drawFlowerPath(Canvas canvas, int contentSize, int N, int pos) {

        float perDegree = 360f / N;

        final float R = contentSize / 2f;
        final float degree = perDegree * pos + this.degree;

        float endY = (float) (R * Math.sin(Math.toRadians(degree)));
        float endX = (float) (R * Math.cos(Math.toRadians(degree)));

        float firstCtlLength = (float) ((R / 2f) / Math.cos(Math.PI / N));
        float leftY = (float) ((firstCtlLength) * Math.sin(degree * Math.PI / 180 - Math.PI / N));
        float leftX = (float) ((firstCtlLength) * Math.cos(degree * Math.PI / 180 - Math.PI / N));

        float rightY = (float) ((firstCtlLength) * Math.sin(degree * Math.PI / 180 + Math.PI / N));
        float rightX = (float) ((firstCtlLength) * Math.cos(degree * Math.PI / 180 + Math.PI / N));

        float topLeftY = leftY + (float) (R / 2f * Math.sin(degree * Math.PI / 180));  //左侧第二控制点
        float topLeftX = leftX + (float) (R / 2f * Math.cos(degree * Math.PI / 180));

        float topRightY = rightY + (float) (R / 2f * Math.sin(degree * Math.PI / 180));  //右侧第二控制点
        float topRightX = rightX + (float) (R / 2f * Math.cos(degree * Math.PI / 180));

        mPath.reset();
        mPath.moveTo(0, 0);

        mPath.cubicTo(leftX, leftY, topLeftX, topLeftY, endX, endY);
        mPath.cubicTo(topRightX, topRightY, rightX, rightY, 0, 0);
        mPath.close();
        if (colorSet == null || colorSet.length == 0) {
            mPaint.setColor(0x9aFB2222);
        } else {
            mPaint.setColor(colorSet[pos % N]);
        }
        mPath.setFillType(Path.FillType.WINDING);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawPath(mPath, mPaint);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(mPath, mPaint);
    }

    public float dip2px(int dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

    public static int argb(float red, float green, float blue) {
        return ((int) (1 * 255.0f + 0.5f) << 24) |
                ((int) (red * 255.0f + 0.5f) << 16) |
                ((int) (green * 255.0f + 0.5f) << 8) |
                (int) (blue * 255.0f + 0.5f);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }

    public void setDegree(float degree) {
        this.degree = degree;
        invalidate();
    }
}

四、总结

和上一篇一样,都是关于贝塞尔曲线的应用,因此掌握贝塞尔曲线是非常重要的,比如自己去设计字体,手写签名都会用到。