Android 路径(Path)与剪裁(Clipping)详解

时间:2024-03-24 18:47:40

转自:http://blog.csdn.net/bigdavidwli007/article/details/8930640

在定义各种形状的view的需求驱使下 ,在网上找到这片文章,貌似看的人很少,所以有必要推广一下。希望对你有用。

在读之前 : 你一定要明白一个道理 :canves 永远都是矩形的 是不可能裁剪成任意形状的,所谓的裁剪只是填充了透明色。也就是canves永远都是以矩形占据视图空间。

个人实践后的见解,请做参考。这篇文章还是很不错的。

自定义view  canves.clipPath ()时 在构造方法里面需要增加  

setLayerType(View.LAYER_TYPE_SOFTWARE, null);

不要硬件加速,不然会出现不正常的显示。

3. 路径(Path)与剪裁(Clipping)

    路径是封装了多条几何轮廓的组合。其中可以包含线段,圆,椭圆,二次和三次的贝塞尔曲线。路径的主要应用场景有:绘制复杂的轮廓,也可以通过这些路径进行剪裁,或者在路径上显示文字。

   

3.1 路径(Path)

3.1.1 绘制复杂轮廓

    使用路径可以轻易的生成一段复杂的曲线,因为Path类提供了丰富的函数来实现此功能。下面罗列并简要解释下这些函数。

    publicvoid addArc(RectF oval, float startAngle, float sweepAngle)。向路径中添加椭圆弧,此椭圆弧的位置由外接矩形oval定义。startAngle定义开始的角度。sweepAngle定义弧线扫描过的角度。

   public void addCircle(float x, float y, float radius, Path.Directiondir)。向路径中添加圆。(x,y)定义圆心的位置。raidus定义圆的半径。dir定义了圆加入路径时的方向。方向由枚举类Path.Direction定义。此参数只用于加入路径的封闭曲线中。

   Path.Direction.CCW 逆时针方向。

   Path.Direction.CW 顺时针方向。

   public void addOval(RectF oval, Path.Direction dir)。向路径中添加椭圆。oval定义了椭圆的位置和大小。Path.Direction定义了椭圆加入路径时的方向。

   public void addPath(Path src, float dx, float dy)。将一段路径加入当前路径。(dx,dy)定义了原路径的偏移量。

   public void addPath(Path src)。将src所指路径加入当前路径。

   public void addPath (Path src, Matrix matrix)。将src所指路径加入当前路径。用matrix所指矩阵进行变换。(变化应用于src还是添加src后的全路径?)

    publicvoid addRect(float left, float top, float right, float bottom, Path.Directiondir)。向路径中添加矩形。

   public void addRect(RectF rect, Path.Direction dir)。向路径中添加矩形。

    publicvoid addRoundRect(RectF rect, float[] radii, Path.Direction dir)。向路径中添加圆角矩形。radii数组定义圆角矩形的四个圆角的x,y半径。radii长度必须为8。

    publicvoid addRoundRect(RectF rect, float rx, float ry, Path.Direction dir)。想路径中添加圆角矩形。rx,ry定义了四个圆角的x,y半径。

    publicvoid arcTo(RectF oval, float startAngle, float sweepAngle)。向路径中添加一段椭圆弧。

arcTo和addArc的区别在于:

   1. 首先,使用addArc可以直接加入一段椭圆弧。而使用arcTo还需要使用moveTo指定当前点的坐标。

   2. 对于arcTo来说,如果当前点坐标和欲添加的曲线的起始点不是同一个点的话,还会自动添加一条直线补齐路径。

    publicvoid moveTo(float x, float y)。移动当前点到(x,y)。

   public void rMoveTo(float dx, float dy)。

    publicvoid lineTo(float x, float y)。从当前路径结束点添加一条路径到指定点(x,y)。

   public void rLineTo(float dx, float dy)。同LineTo,区别在于LineTo中的(x,y)是对应于坐标原点。而此处(dx,dy)是对应于路径结束点的相对坐标。其他r*函数都类似。

   public void quadTo(float x1, float y1, float x2, float y2)。以当前路径结束点为开始点,(x1,y1)为控制点,(x2,y2)为结束点画一条二次贝塞尔曲线。

   public void rQuadTo(float dx1, float dy1, float dx2, float dy2)。同quadTo。

   public void cubicTo(float x1, float y1, float x2, float y2, float x3,float y3)。以当前路径结束点为开始点,(x1,y1),(x2,y2)为控制点,(x3,y3)为结束点画一条三次贝塞尔曲线。

   public void rCubicTo(float x1, float y1, float x2, float y2, float x3,float y3)。同cubicTo。.

    下面是相关示例代码和运行效果截图:

   Android 路径(Path)与剪裁(Clipping)详解 

   package com.example.androidgraphicsprogramm;


import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Bundle;
import android.view.View;


public class PathBasic extends Activity {
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SampleView(this));
    }
    
    private static class SampleView extends View {
        private Paint mPaint;
        Path path;


        public SampleView(Context context) {
            super(context);
            setFocusable(true);
            
            mPaint = new Paint();
            path = new Path();
        }
        
        
        @Override protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.WHITE);        
            
            drawPath_1(canvas);
            drawPath_2(canvas);
        }
        
        public void drawPath_1(Canvas canvas) {
        Paint paint = mPaint;
        int xoffset = 100;
        int yoffset = 100;
        
        paint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        
        canvas.drawPoint(xoffset,yoffset, paint);
        
        paint.setColor(Color.RED);
        
        canvas.save();
        canvas.translate(xoffset, yoffset);
        path.reset();
        RectF rect = new RectF();
        rect.left = 0;
        rect.top = 0;
        rect.right = rect.left + 200;
        rect.bottom = rect.top + 100;
               
        path.addArc(rect, 0,180);
        
        rect.left = 250;
        rect.top = 0;
        rect.right = rect.left + 200;
        rect.bottom = rect.top + 100;
            path.addArc(rect, 180, 180);    
            
        canvas.drawPath(path, paint);        
        canvas.restore();
        
        yoffset += 200;
        canvas.save();
        canvas.translate(xoffset, yoffset);
        
        path.reset();
        
        rect.left = 0;
        rect.top = 0;
        rect.right = rect.left + 200;
        rect.bottom = rect.top + 100;
        
        path.addOval(rect,Path.Direction.CCW);
        
        
        rect.left = 150;
        rect.top = 0;
        rect.right = rect.left + 200;
        rect.bottom = rect.top + 100;
        
        path.addRoundRect(rect,10,5,Path.Direction.CCW);
        canvas.drawPath(path, paint);        
        canvas.restore();
        
        // 
        yoffset += 200;
        canvas.save();
        canvas.translate(xoffset, yoffset);
        
        path.reset();
        path.setFillType(Path.FillType.EVEN_ODD);
        mPaint.setStyle(Paint.Style.FILL);
        
        rect.left = 0;
        rect.top = 0;
        rect.right = rect.left + 200;
        rect.bottom = rect.top + 100;
        
        path.addOval(rect,Path.Direction.CCW);
               
        rect.left = 150;
        rect.top = 0;
        rect.right = rect.left + 200;
        rect.bottom = rect.top + 100;
        
        path.addRoundRect(rect,10,5,Path.Direction.CCW);
        canvas.drawPath(path, paint);        
        canvas.restore();
        }
        
        public void drawPath_2(Canvas canvas) {
        Paint paint = mPaint;
        
        int xoffset = 100;
        int yoffset = 700;
        
        canvas.save();
        canvas.translate(xoffset, yoffset);
        
        path.reset();
        //path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        
        path.moveTo(0, 0);
        path.lineTo(200, 200);
        
        RectF rect = new RectF();
        rect.left = 200;
        rect.top = 100;
        rect.right = rect.left + 200;
        rect.bottom = rect.top + 200;
        
        path.arcTo(rect, 180, 180);
        path.rLineTo(200, -200);
        path.rQuadTo(-150, 100, -300, 0);
        canvas.drawPath(path, paint);
        }
    
    }
}

 

 

3.1.2 路径的特效

 

   android 针对path对象提供了一系列的PathEffect类来实现path绘制的特效。想实现path的特殊绘制效果,需要创建一个响应的PathEffect类,然后调用Paint的setPathEffect方法,将PathEffect类的对象作为参数传入,这样在绘制path时就可以根据PathEffect产生响应的效果了。下面详细介绍下相关的PathEffect类。

1. CornerPathEffect(float radius), 圆角效果。将path中的两条线段形成的锐角夹角用圆弧替代。CornerPathEffect的构造函数的参数指定了圆弧的半径。

   注释:只对锐角有效,这是Android官方文档描述的,原文如下:Transformsgeometries that are drawn (either STROKE or FILL styles) by replacing any sharpangles between line segments into rounded angles of the specified radius.但是从我的实验结果看,好像钝角也会被处理。

 

2. DashPathEffect(float[] intervals, floatphase)虚线效果。用虚线绘制路径。构造函数中的参数含义分别为:intervals: 定义path中的虚线单元。s数组的长度必须大于等于2,而且必须为偶数。phase 表示绘制path是的偏移量。

    例如,针对本文提供的例子:

    e[2] = new DashPathEffect(newfloat[] {10, 5, 5, 5}, phase);
(10,5,5,5)描述了一条由两条线段和两端空白组成的虚线。整条path就由这个虚线组成。

10 表示第一段线段长度为10,5表示第一段线段之后的空白长度为5,第三个5表示第二条线段长度为5,第四个5表示第二段线段之后的空白长度为5. phase表示绘制是的偏移量,如果phase值为5,则表示绘制时会从第一条长度为10的线段的长度为5的地方开始绘制。因此不断改变phase的值就能实现曲线的动画效果。

 

3. public DiscretePathEffect (floatsegmentLength, float deviation)。将原路径切成segmentLength长度定义的线段,并且每条线段在原路径上以deviation指定的偏移量(角度?弧度?)进行偏移。

 

4. public PathDashPathEffect (Path shape, float advance, float phase,PathDashPathEffect.Style style)。定义一个新的形状(路径)并将其用作原始路径的轮廓标记。参数说明如下:

shape: 定义一个新的形状,绘制出来的路径由一个个这样的形状组成。

advance: 定义两个形状之间的距离。

phase: 定义偏移量,同DashPathEffect种的phase参数含义。

style:定义了图像单元的方向,效果见下图:

     第一条蓝色的曲线style取值为:PathDashPathEffect.Style.TRANSLATE

     第二条蓝色的曲线style取值为:PathDashPathEffect.Style. MORPH

     第三条蓝色的曲线style取值为:PathDashPathEffect.Style. ROTATE

Android 路径(Path)与剪裁(Clipping)详解

 

5. public ComposePathEffect (PathEffectouterpe, PathEffect innerpe)。定义了两种效果的复合作用。

 

6. public SumPathEffect (PathEffect first,PathEffect second)。也定义了两种效果的组合作用。但是和ComposePathEffect有区别。ComposePathEffect 效果是两种效果的组合,如一个圆角和一个虚线,显示的结果是一条圆角虚线的路径。SumPathEffect的效果则是两种效果的简单叠加。就是用第一种效果先画一遍,再用第二种效果画一遍。下面的效果图是一个很好的说明。

上图:

Android 路径(Path)与剪裁(Clipping)详解

上代码:

package com.example.androidgraphicsprogramm;


import android.app.Activity;
import android.content.Context;
import android.graphics.*;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;


public class PathEffects extends Activity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SampleView(this));
    }
    
    private static class SampleView extends View {
        private Paint mPaint;
        private Path mPath;
        private PathEffect[] mEffects;
        private int[] mColors;
        private float mPhase = 0;


//        private static PathEffect makeDash(float phase) {
//            return new DashPathEffect(new float[] { 15, 5, 8, 5 }, phase);
//        }
        
        private static void makeEffects(PathEffect[] e, float phase) {
            e[0] = null;     // no effect
            e[1] = new CornerPathEffect(10);
            e[2] = new DashPathEffect(new float[] {10, 5, 5, 5}, phase);
//            e[2] = new PathDashPathEffect(makePathDash(), 12, phase,
//                    PathDashPathEffect.Style.TRANSLATE);
            
            e[3] = new PathDashPathEffect(makePathDash(), 12, phase,
                                          PathDashPathEffect.Style.MORPH);
//            e[4] = new PathDashPathEffect(makePathDash(), 12, phase,
//                    PathDashPathEffect.Style.ROTATE);
            e[4] = new ComposePathEffect(e[2], e[1]);
            e[5] = new SumPathEffect(e[2], e[1]);
            e[6] = new DiscretePathEffect(25,4);
        }
        
        public SampleView(Context context) {
            super(context);
            setFocusable(true);
            setFocusableInTouchMode(true);


            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(6);
            
            mPath = makeFollowPath();
            
            mEffects = new PathEffect[7];
            
            mColors = new int[] { Color.BLACK, Color.RED, Color.BLUE,
                                  Color.GREEN, Color.MAGENTA, Color.BLACK,Color.RED
                                };
        }
        
        @Override protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.WHITE);
            
            RectF bounds = new RectF();
            mPath.computeBounds(bounds, false);
            canvas.translate(10 - bounds.left, 10 - bounds.top);
            
            makeEffects(mEffects, mPhase);
            //mPhase ++;
            invalidate();


            for (int i = 0; i < mEffects.length; i++) {
                mPaint.setPathEffect(mEffects[i]);
                mPaint.setColor(mColors[i]);
                canvas.drawPath(mPath, mPaint);
                canvas.translate(0, 100);
            }
        }
        
        @Override public boolean onKeyDown(int keyCode, KeyEvent event) {
            switch (keyCode) {
                case KeyEvent.KEYCODE_DPAD_CENTER:
                    mPath = makeFollowPath();
                    return true;
            }
            return super.onKeyDown(keyCode, event);
        }


        private static Path makeFollowPath() {
            Path p = new Path();
            p.moveTo(0, 0);
            for (int i = 1; i <= 20; i++) {
                p.lineTo(i*20, (float)Math.random() * 100);
            }
            return p;
        }
        
        private static Path makePathDash() {
            Path p = new Path();
            p.moveTo(4, 0);
            p.lineTo(0, -4);
            p.lineTo(8, -4);
            p.lineTo(12, 0);
            p.lineTo(8, 4);
            p.lineTo(0, 4);
            return p;
        }
    }
}
 

3.1.3 路径的填充

 

android 在绘制多边形时可以指定填充方式。主要涉及的函数和参数有:

1. Path 类提供的设置填充方式的函数:

   public void setFillType(Path.FillType ft).

2. 填充方式的枚举类型:

    Path.FillType   EVEN_ODD,用奇偶规则填充

    Path.FillType   INVERSE_EVEN_ODD,顾名思义,和EVEN_ODD规则恰好相反。

    Path.FillType   INVERSE_WINDING,同样,WINDING的反效果。

    Path.FillType   WINDING,用非零环绕数规则填充。

 

    下面详细说明下什么是奇偶规则和非零环绕数规则:

    奇偶规则和非零环绕数都是计算机图形学中用来判断一个点是否在多边形内部的算法。奇偶规则是指:对任意点P,从P到无穷远处画一条射线,注意该射线不能喝多边形的任何顶点相交,计算该射线和多边形的各条边的交点个数,如果交点数目为奇数,则认为P点再多边形内,反之,为偶数则认为P点再多边形外。非零环绕数规则是指从任意点P同样想无穷远处画一条射线,同样不能喝任何顶点相交。首先初始化环绕边数为0,当P点沿射线方向移动时,统计该穿过该射线的边的方向。每当多边形从右到左穿过射线时,边数加1,反之减1。最后如果环绕边数为非0,则认为P是内部点,为0则认为P是外部点。下图是一个关于这两个规则的示意图:

Android 路径(Path)与剪裁(Clipping)详解
下面是示例代码的运行结果截图和源码:

Android 路径(Path)与剪裁(Clipping)详解

 

源码:

package com.example.androidgraphicsprogramm;

 

import android.app.Activity;

import android.content.Context;

import android.graphics.*;

import android.os.Bundle;

import android.view.View;

 

public classPathFillType extends Activity {

 

    @Override

   protectedvoidonCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(new SampleView(this));

   }

   

   privatestaticclassSampleView extendsView {

        private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        private Path mPath;

 

        public SampleView(Context context) {

            super(context);

            setFocusable(true);

            setFocusableInTouchMode(true);

 

            mPath = new Path();

            mPath.addCircle(40, 40, 45, Path.Direction.CCW);

            mPath.addCircle(80, 80, 45, Path.Direction.CCW);

        }

       

        private void showPath(Canvas canvas, int x, int y, Path.FillType ft,

                              Paint paint) {

            canvas.save();

            canvas.translate(x, y);

            canvas.clipRect(0, 0, 120, 120);

            canvas.drawColor(Color.WHITE);

           mPath.setFillType(ft);

            canvas.drawPath(mPath, paint);

            canvas.restore();

        }

       

        @Override protected voidonDraw(Canvas canvas) {

            Paint paint = mPaint;

 

            canvas.drawColor(0xFFCCCCCC);

           

            canvas.translate(20, 20);

           

            paint.setAntiAlias(true);

 

            showPath(canvas, 0, 0,Path.FillType.WINDING, paint);

            showPath(canvas, 160, 0,Path.FillType.EVEN_ODD, paint);

            showPath(canvas, 0, 160,Path.FillType.INVERSE_WINDING, paint);

            showPath(canvas, 160, 160,Path.FillType.INVERSE_EVEN_ODD, paint);

        }

   }

}

3.2 剪裁

  剪裁是Canvas的重要功能之一。可以剪切画裁的指定区域。并且可以对多次剪切的结果进行组合,实现组合效果。android画布类提供了如下函数实现剪切功能。

  boolean        clipPath(Path path)。用指定路径剪切画布。

  boolean        clipPath(Path path, Region.Op op)。用指定路径剪裁画布,并根据Region.Op标记对多次剪裁结果进行组合。

  boolean        clipRect(Rect rect, Region.Op op)。用指定矩形剪裁画布,并根据Region.Op标记对多次剪裁结果进行组合。

  boolean        clipRect(RectF rect, Region.Op op)。用指定矩形剪裁画布,并根据Region.Op标记对多次剪裁结果进行组合。

  boolean        clipRect(int left, int top, int right,int bottom)。用指定矩形剪裁画布。

  boolean        clipRect(float left, float top, floatright, float bottom) 。用指定矩形剪裁画布。

  boolean        clipRect(RectF rect)。用指定矩形剪裁画布。

  boolean        clipRect(float left, float top, floatright, float bottom, Region.Op op)。用指定举行剪裁画布。并根据Region.Op标记对多次剪裁结果进行组合。

  boolean        clipRect(Rect rect)。用指定矩形剪裁画布。

  boolean        clipRegion(Region region)。用指定区域剪裁画布。

  boolean        clipRegion(Region region, Region.Op op)。用指定区域剪裁画布。并根据Region.Op标记对多次剪裁结果进行组合。

 

Region.Op 表示多次裁剪的组合方式,有如下选择:

 

假设有两次clip操作,结果分别为集合A 和 B,那么下面的操作方式分别表示:

Region.Op       DIFFERENCE   A- B

Region.Op       INTERSECT      A∩B

Region.Op       REPLACE          B

Region.Op       REVERSE_DIFFERENCE  B- A

Region.Op       UNION    A∪ B

Region.Op       XOR (A∪B)- (A∩B)

 

效果图如下:

Android 路径(Path)与剪裁(Clipping)详解

 

代码如下:

 

packagecom.example.androidgraphicsprogramm;

 

import android.app.Activity;

import android.content.Context;

import android.graphics.*;

import android.os.Bundle;

import android.util.Log;

import android.view.View;

 

public class Clipping extends Activity {

 

         privatestatic final String TAG = "Clipping";

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(new SampleView(this));

    }

   

   private static class SampleView extends View {

       private Paint mPaint;

       private Path mPath;

 

        public SampleView(Context context) {

           super(context);

           setFocusable(true);

           

           mPaint = new Paint();

           mPaint.setAntiAlias(true);

           mPaint.setStrokeWidth(6);

           mPaint.setTextSize(16);

           mPaint.setTextAlign(Paint.Align.RIGHT);

           

           mPath = new Path();

       }

       

       private void drawScene(Canvas canvas) {

           canvas.clipRect(0, 0, 160, 160);

           

           canvas.drawColor(Color.WHITE);

           

           mPaint.setColor(Color.RED);

           canvas.drawLine(0, 0, 160, 160, mPaint);

           

           mPaint.setColor(Color.GREEN);

           canvas.drawCircle(47, 113, 47, mPaint);

           

           mPaint.setColor(Color.BLUE);

           canvas.drawText("Clipping", 140, 30, mPaint);

       }

       

       @Override protected void onDraw(Canvas canvas) {

           boolean clipresult = false;

           

                canvas.drawColor(Color.GRAY);           

 

           canvas.save();

           canvas.translate(10, 10);

           drawScene(canvas);

           canvas.restore();

           

           canvas.save();

           canvas.translate(180, 10);

           clipresult = canvas.clipRect(20, 20, 140, 140);

           Log.d(TAG,"clip result is: " + clipresult);

           clipresult = canvas.clipRect(50, 50, 110, 110, Region.Op.DIFFERENCE);

           Log.d(TAG,"clip result with op is: " + clipresult);

           drawScene(canvas);

           canvas.restore();

           

          

           canvas.save();

           canvas.translate(10, 200);

           canvas.clipRect(20, 20, 140, 140);

           mPath.reset();

           canvas.clipPath(mPath); // makes the clip empty

           mPath.addCircle(80, 80, 80, Path.Direction.CCW);

           canvas.clipPath(mPath, Region.Op.REPLACE);

           drawScene(canvas);

           canvas.restore();

           

           

           canvas.save();

           canvas.translate(180, 200);

           canvas.clipRect(10, 10, 100, 100);

           canvas.clipRect(80, 80, 170, 170, Region.Op.UNION);

           drawScene(canvas);

           canvas.restore();

           

           

           canvas.save();

           canvas.translate(10, 390);

           canvas.clipRect(0, 0, 80, 80);

           canvas.clipRect(40, 40, 120, 120, Region.Op.XOR);

           drawScene(canvas);

           canvas.restore();

           

           

           canvas.save();

           canvas.translate(180, 390);

           canvas.clipRect(0, 0, 80, 80);

           canvas.clipRect(40, 40, 120, 120, Region.Op.REVERSE_DIFFERENCE);

           drawScene(canvas);

           canvas.restore();

           

           

           canvas.save();

           canvas.translate(10, 580);

           canvas.clipRect(0, 0, 80, 80);

           canvas.clipRect(40, 40, 120, 120, Region.Op.INTERSECT);

           drawScene(canvas);

           canvas.restore();

           

       }

    }

}