1. 程式人生 > >Android 自定義Path貝塞爾曲線View實踐——旋轉的花朵

Android 自定義Path貝塞爾曲線View實踐——旋轉的花朵

開發十年,就只剩下這套架構體系了! >>>   

一、關於貝塞爾曲線

在工業設計方面貝塞爾曲線有很多用途,同樣,在Android中,貝塞爾曲線結合Path類可以實現更復雜的圖形,這裡我們給一個案例,來實現一種旋轉的花朵。對於貝賽爾曲線的理解,建議參考《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-旋轉的花朵


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 int[] colorSet = new int[]{0xffFF1493,0xffFFD700,0xffFFFF00,0xff87CEFA,0xff00FA9A,0xffBA55D3,0xffE0FFFF};

    private boolean isPlaying = false;

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

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

    public FlowerView(Context context, @Nullable 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);  //取最小邊長,防止畫出邊界


        clearCanvas(canvas);

        canvas.drawColor(0xffffffff);

        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;
        this.colorSet = colorSet;
        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 new Float(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.setColor(Color.RED);
            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawPath(mPath,mPaint);
    }


    private synchronized void clearCanvas(Canvas canvas) {

        final Xfermode xfermode = mPaint.getXfermode();
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        canvas.drawPaint(mPaint);
        mPaint.setXfermode(xfermode);
    }


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

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

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

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

以上就是整個View的實現,使用方法如下

      myFlower = (FlowerView) findViewById(R.id.id_myflower);
        myFlower.setPetalNumber(7,null);
        myFlower.setOnClickListener(this);
  
  //省略其他不需要給你們看的程式碼
    @Override
    public void onClick(View v) {
        if(v.getId()==R.id.id_myflower){
            myFlower.startRotate();
        }
    }