Android自定义控件学习笔记(二)

自定义控件系列的读书笔记,整理自下列资料,不代表博主个人观点 :

GcsSloop/AndroidNote


三、Paint

3.1 简单介绍paint

绘制的基本形状由Canvas确定,但绘制出来的颜色,具体效果则由Paint确定

如果你注意到了的话,在一开始我们设置画笔样式的时候是这样的:

//设置画笔模式为填充
mPaint.setStyle(Paint.Style.FILL); 

为了展示方便,容易看出效果,之前使用的模式一直为填充模式,实际上画笔有三种模式,如下:

STROKE                //描边
FILL                  //填充
FILL_AND_STROKE       //描边加填充

为了区分三者效果我们做如下实验:

Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeWidth(40);
 
// 描边
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(200,200,100,paint);
 
// 填充
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(200,500,100,paint);
 
// 描边加填充
paint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(200, 800, 100, paint);

结果如图所示:


3.2 绘制文本的Paint

绘制文字也是需要画笔的,而且文字的大小,颜色,字体,对齐方式都是由画笔控制的。

Paint文本相关常用方法表

操作类型 相关API 备注
色彩 setColor setARGB setAlpha 设置颜色,透明度
大小 setTextSize 设置文本字体大小
字体 setTypeface 设置或清除字体样式
样式 setStyle 填充(FILL),描边(STROKE),填充加描边(FILL_AND_STROKE)
对齐 setTextAlign 左对齐(LEFT),居中对齐(CENTER),右对齐(RIGHT)
测量 measureText 测量文本大小(注意,请在设置完文本各项参数后调用)

四、使用Canvas绘制基本形状

4.1 Canvas简介

Canvas我们可以称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础,非常强大。

4.2 Canvas的常用操作速查表

操作类型 相关API 备注
绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布
绘制基本形状 drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
绘制图片 drawBitmap, drawPicture 绘制位图和图片
绘制文本 drawText, drawPosText, drawTextOnPath 依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字
绘制路径 drawPath 绘制路径,绘制贝塞尔曲线时也需要用到该函数
顶点操作 drawVertices, drawBitmapMesh 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用
画布剪裁 clipPath, clipRect 设置画布的显示区域
画布快照 save, restore, saveLayerXxx, restoreToCount, getSaveCount 依次为 保存当前状态、回滚到上一次保存的状态、保存图层状态、回滚到指定状态、获取保存次数
画布变换 translate, scale, rotate, skew 依次为 位移、缩放、旋转、倾斜
Matrix(矩阵) getMatrix, setMatrix, concat 实际画布的位移,缩放等操作的都是图像矩阵Matrix,只不过Matrix比较难以理解和使用,故封装了一些常用的方法。

4.3 Canvas基础操作

(1)绘制颜色

绘制颜色是填充整个画布,常用于绘制底色。

canvas.drawColor(Color.BLUE); //绘制蓝色

(2)创建画笔

要想绘制内容,首先需要先创建一个画笔,如下:

// 1.创建一个画笔
private Paint mPaint = new Paint();
 
// 2.初始化画笔
private void initPaint() {
    mPaint.setColor(Color.BLACK);       //设置画笔颜色
    mPaint.setStyle(Paint.Style.FILL);  //设置画笔模式为填充
    mPaint.setStrokeWidth(10f);         //设置画笔宽度为10px
}

// 3.在构造函数中初始化
public SloopView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initPaint();
}

在创建完画笔之后,就可以在Canvas中绘制各种内容了。

(3)绘制点

可以绘制一个点,也可以绘制一组点,如下:

canvas.drawPoint(200, 200, mPaint);     //在坐标(200,200)位置绘制一个点
canvas.drawPoints(new float[]{          //绘制一组点,坐标位置由float数组指定
                500,500,
                500,600,
                500,700
},mPaint);

关于坐标原点默认在左上角,水平向右为x轴增大方向,竖直向下为y轴增大方向。

(3)绘制直线

绘制直线需要两个点,初始点和结束点,同样绘制直线也可以绘制一条或者绘制一组:

canvas.drawLine(300,300,500,600,mPaint);    // 在坐标(300,300)(500,600)之间绘制一条直线
canvas.drawLines(new float[]{               // 绘制一组线 每四数字(两个点的坐标)确定一条线
                100,200,200,200,
                100,300,200,300
},mPaint);

(4)绘制矩形

确定一个矩形最少需要四个数据,就是对角线的两个点的坐标值,这里一般采用左上角和右下角的两个点的坐标。

关于绘制矩形,Canvas提供了三种重载方法,第一种就是提供四个数值(矩形左上角和右下角两个点的坐标)来确定一个矩形进行绘制。 其余两种是先将矩形封装为Rect或RectF(实际上仍然是用两个坐标点来确定的矩形),然后传递给Canvas绘制,如下:

// 第一种
canvas.drawRect(100,100,800,400,mPaint);
 
// 第二种
Rect rect = new Rect(100,100,800,400);
canvas.drawRect(rect,mPaint);
 
// 第三种
RectF rectF = new RectF(100,100,800,400);
canvas.drawRect(rectF,mPaint);

Rect是int(整形)的,而RectF是float(单精度浮点型)的。除了精度不同,两种提供的方法也稍微存在差别。

(5)绘制圆角矩形

绘制圆角矩形也提供了两种重载方式,如下:

// 第一种
RectF rectF = new RectF(100,100,800,400);
canvas.drawRoundRect(rectF,30,30,mPaint);
 
// 第二种
canvas.drawRoundRect(100,100,800,400,30,30,mPaint);

与矩形相比,圆角矩形多出来了两个参数rx 和 ry,这两个参数是干什么的呢?既然是圆角矩形,他的角肯定是圆弧(圆形的一部分),这里的两个参数实际上是椭圆的两个半径。当rx大于宽度的一半,ry大于高度的一半时,实际上是无法计算出圆弧的,所以drawRoundRect对大于该数值的参数进行了限制(修正),凡是大于一半的参数均按照一半来处理。如下图所示:

(6)绘制椭圆

相对于绘制圆角矩形,绘制椭圆就简单的多了,因为他只需要一个矩形矩形作为参数:

// 第一种
RectF rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);
 
// 第二种
canvas.drawOval(100,100,800,400,mPaint);

(7)绘制圆

绘制圆形也比较简单, 如下:

// 绘制一个圆心坐标在(500,500),半径为400 的圆。
canvas.drawCircle(500,500,400,mPaint);  

绘制圆形有四个参数,前两个是圆心坐标,第三个是半径,最后一个是画笔。

(8)绘制圆弧

// 第一种
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){}
 
// 第二种
public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {}

从上面可以看出,相比于绘制椭圆,绘制圆弧还多了三个参数:

startAngle  // 开始角度
sweepAngle  // 扫过角度
useCenter   // 是否使用中心

下面上代码:

RectF rectF = new RectF(100,100,800,400);
// 绘制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF,mPaint);
 
// 绘制圆弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF,0,90,false,mPaint);
 
//-------------------------------------
 
RectF rectF2 = new RectF(100,600,800,900);
// 绘制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF2,mPaint);
 
// 绘制圆弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF2,0,90,true,mPaint);

上述代码实际上是绘制了一个起始角度为0度,扫过90度的圆弧,两者的区别就是是否使用了中心点,结果如下:

可以发现使用了中心点之后绘制出来类似于一个扇形,而不使用中心点则是圆弧起始点和结束点之间的连线加上圆弧围成的图形。这样中心点这个参数的作用就很明显了。

4.4 简单示例

参考:http://android.jobbole.com/83342/

4.5 Canvas进阶:绘制图片

绘制有两种方法,drawPicture(矢量图) 和 drawBitmap(位图)。

(1)drawPicture

使用Picture前请关闭硬件加速,以免引起不必要的问题!!!!!!!!!!

myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

也可以在AndroidMenifest文件中application节点下添上 android:hardwareAccelerated="false"以关闭整个应用的硬件加速。 更多请参考这里:Android的硬件加速及可能导致的问题

这里的 Picture 并不是我们理解中的Picture,我们把Canvas绘制点,线,矩形等诸多操作用Picture录制下来,下次需要的时候拿来就能用,使用Picture相比于再次调用绘图API,开销是比较小的,也就是说对于重复的操作可以更加省时省力。

PS:可以把Picture看作是一个录制Canvas操作的录像机。

了解了Picture的概念之后,我们再了解一下Picture的相关方法:

相关方法 简介
public int getWidth () 获取宽度
public int getHeight () 获取高度
public Canvas beginRecording (int width, int height) 开始录制 (返回一个Canvas,在Canvas中所有的绘制都会存储在Picture中)
public void endRecording () 结束录制
public void draw (Canvas canvas) 将Picture中内容绘制到Canvas中
public static Picture createFromStream (InputStream stream) (已废弃)通过输入流创建一个Picture
public void writeToStream (OutputStream stream) (已废弃)将Picture中内容写出到输出流中

很明显,beginRecording 和 endRecording 是成对使用的,一个开始录制,一个是结束录制,两者之间的操作将会存储在Picture中。

录制内容,即将一些Canvas操作用Picture存储起来,录制的内容是不会直接显示在屏幕上的,只是存储起来了而已。

录制的实例:

    // 1.创建Picture
    private Picture mPicture = new Picture();
    
---------------------------------------------------------------
    
    // 2.录制内容方法
    private void recording() {
        // 开始录制 (接收返回值Canvas)
        Canvas canvas = mPicture.beginRecording(500, 500);
        // 创建一个画笔
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);

        // 在Canvas中具体操作
        // 位移
        canvas.translate(250,250);
        // 绘制一个圆
        canvas.drawCircle(0,0,100,paint);

        mPicture.endRecording();
    }
    
---------------------------------------------------------------

    // 3.在使用前调用(我在构造函数中调用了)
      public Canvas3(Context context, AttributeSet attrs) {
        super(context, attrs);
        
        recording();    // 调用录制
    }

具体使用:

Picture虽然方法就那么几个,但是具体使用起来还是分很多情况的,由于录制的内容不会直接显示,就像存储的视频不点击播放不会自动播放一样,同样,想要将Picture中的内容显示出来就需要手动调用播放(绘制),将Picture中的内容绘制出来可以有以下几种方法:

序号 简介 是否对Canvas有影响
1 使用Picture提供的draw方法绘制
2 使用Canvas提供的drawPicture方法绘制
3 将Picture包装成为PictureDrawable,使用PictureDrawable的draw方法绘制

使用Picture提供的draw方法绘制:

// 将Picture中的内容绘制在Canvas上
mPicture.draw(canvas);  

PS:这种方法在比较低版本的系统上绘制后可能会影响Canvas状态,所以这种方法一般不会使用。

使用Canvas提供的drawPicture方法绘制:

drawPicture有三种方法:

public void drawPicture (Picture picture)
public void drawPicture (Picture picture, Rect dst)
public void drawPicture (Picture picture, RectF dst)

和使用Picture的draw方法不同,Canvas的drawPicture不会影响Canvas状态。

canvas.drawPicture(mPicture,new RectF(0,0,mPicture.getWidth(),200));

PS:对照上一张图片,可以比较明显的看出,绘制的内容根据选区进行了缩放。

将Picture包装成为PictureDrawable,使用PictureDrawable的draw方法绘制:

// 包装成为Drawable
PictureDrawable drawable = new PictureDrawable(mPicture);
// 设置绘制区域 -- 注意此处所绘制的实际内容不会缩放
drawable.setBounds(0,0,250,mPicture.getHeight());
// 绘制
drawable.draw(canvas);

PS:此处setBounds是设置在画布上的绘制区域,并非根据该区域进行缩放,也不是剪裁Picture,每次都从Picture的左上角开始绘制。

(2)drawBitmap

通过BitmapFactory从不同位置获取Bitmap:

资源文件(drawable/mipmap/raw):

Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),R.raw.bitmap);

资源文件(assets):

Bitmap bitmap=null;
try {
     InputStream is = mContext.getAssets().open("bitmap.png");
     bitmap = BitmapFactory.decodeStream(is);
     is.close();
} catch (IOException e) {
     ....
}

内存卡文件:

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");

网络文件:

// 此处省略了获取网络输入流的代码
Bitmap bitmap = BitmapFactory.decodeStream(is);
is.close();

绘制Bitmap:

// 第一种
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
    
// 第二种
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
    
// 第三种
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)

第一种方法中后两个参数(matrix, paint)是在绘制的时候对图片进行一些改变,如果只是需要将图片内容绘制出来只需要如下操作就可以了:

canvas.drawBitmap(bitmap,new Matrix(),new Paint());

第二种方法就是在绘制时指定了图片左上角的坐标(距离坐标原点的距离):

canvas.drawBitmap(bitmap,200,500,new Paint());

注意:此处指定的是与坐标原点的距离,并非是与屏幕顶部和左侧的距离, 虽然默认状态下两者是重合的,但是也请注意分别两者的不同。

Paste_Image.png

第三种方法比较有意思,上面多了两个矩形区域(src,dst),这两个矩形选区是干什么用的?

示例:

// 将画布坐标系移动到画布中央
canvas.translate(mWidth/2,mHeight/2);

// 指定图片绘制区域(左上角的四分之一)
Rect src = new Rect(0,0,bitmap.getWidth()/2,bitmap.getHeight()/2);

// 指定图片在屏幕上显示的区域
Rect dst = new Rect(0,0,200,400);

// 绘制图片
canvas.drawBitmap(bitmap,src,dst,null);

详解:

上面是以绘制该图为例,用src指定了图片绘制部分的区域,即下图中红色方框标注的区域。

然后用dst指定了绘制在屏幕上的绘制,即下图中蓝色方框标注的区域,图片宽高会根据指定的区域自动进行缩放。

从上面可知,第三种方法可以绘制图片的一部分到画布上,这有什么用呢?如果你看过某些游戏的资源文件,你可能会看到如下的图片:

这样的话,就可以用一张图片包含了大量的素材,在绘制的时候每次只截取一部分进行绘制,这样可以大大的减少素材数量,而且素材管理起来也很方便。

下面是利用drawBitmap第三种方法制作的一个简单示例:

资源文件如下:

最终效果如下:

源码如下:点击此处查看源码

4.6 Canvas绘制文字

预览一下相关常用方法:

// 第一类
public void drawText (String text, float x, float y, Paint paint)
public void drawText (String text, int start, int end, float x, float y, Paint paint)
public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
public void drawText (char[] text, int index, int count, float x, float y, Paint paint)
    
// 第二类
public void drawPosText (String text, float[] pos, Paint paint)
public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)
    
// 第三类
public void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)
public void drawTextOnPath (char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)

绘制文字部分大致可以分为三类:

第一类只能指定文本基线位置(基线x默认在字符串左侧,基线y默认在字符串下方)。
第二类可以分别指定每个文字的位置。
第三类是指定一个路径,根据路径绘制文字。

绘制文字也是需要画笔的,而且文字的大小,颜色,字体,对齐方式都是由画笔控制的。(这个在第三节已经介绍过)

为了绘制文本,我们先创建一个文本画笔:

Paint textPaint = new Paint();          // 创建画笔
textPaint.setColor(Color.BLACK);        // 设置颜色
textPaint.setStyle(Paint.Style.FILL);   // 设置样式
textPaint.setTextSize(50);              // 设置字体大小
(1)第一类(drawText)

第一类可以指定文本开始的位置,可以截取文本中部分内容进行绘制。其中x,y两个参数是指定文本绘制两个基线,示例:

// 文本(要绘制的内容)
String str = "ABCDEFGHIJK";

// 参数分别为 (文本 基线x 基线y 画笔)
canvas.drawText(str,200,500,textPaint);

PS: 图中字符串下方的红线是基线y,基线x未在图中画出。

当然啦,除了能指定绘制文本的起始位置,还能只取出文本中的一部分内容进行绘制。

// 文本(要绘制的内容)
String str = "ABCDEFGHIJK";
// 参数分别为 (字符串 开始截取位置 结束截取位置 基线x 基线y 画笔)
canvas.drawText(str,1,3,200,500,textPaint);

这样绘制的结果是:BC

PS:一定要注意接口中参数 String 和 CharSequence 的两个方法的不同,前者是使用 start和end 来截取字符串,后者是采用 startIndex 和 count 来截取字符数组。

// 字符数组(要绘制的内容)
char[] chars = "ABCDEFGHIJK".toCharArray();
        
// 参数为 (字符数组 起始坐标 截取长度 基线x 基线y 画笔)
canvas.drawText(chars,1,3,200,500,textPaint);

这样绘制的结果是:BCD

(2)第二类(drawPosText)

通过和第一类比较,我们可以发现,第二类中没有指定x,y坐标的参数,而是出现了这样一个参数float[] pos。这个名为pos的浮点型数组就是指定坐标的,可以给每个字符都指定一个位置。

String str = "SLOOP";

canvas.drawPosText(str,new float[]{
         100,100,    // 第一个字符位置
         200,200,    // 第二个字符位置
         300,300,    // ...
         400,400,
         500,500
},textPaint);

不建议用:因为性能不佳,在大量使用的时候可能导致卡顿;必须指定所有字符位置,否则直接crash掉,反人类设计;不支持emoji等特殊字符,不支持字形组合与分解。

(3)第三类(drawTextOnPath)

由于需要用到Path,所以放到后续章节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容