搜索
您的当前位置:首页正文

Paint进阶

来源:哗拓教育

目录

目录

Android已经为我们提供了大量的View供我们使用,但是可能有时候这些组件不能满足我们的开发中的需求,这时候就需要自定义控件了,而自定义控件主要是调用系统提供的API来实现我们需要的样式。本文主要介绍Paint(画笔)相关API的使用。

下文API的说明都是以Demo示例来说明,有关Paint的基础Api这里不再阐述,统一设置Paint的基础样式如下:

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//抗锯齿
mPaint.setColor(Color.parseColor("#FF4081"));//设置画笔的颜色
mPaint.setStyle(Paint.Style.STROKE);//设置样式为描边
mPaint.setStrokeWidth(42);//设置描边的宽度,这里为演示方便,直接使用px

API

  • setStrokeCap(Cap cap) : 设置线冒(或者笔触)的样式

什么是笔触?其实很简单,就像我们现实世界中的笔,如果你用圆珠笔在纸上戳一点,那么这个点一定是个圆,即便很小,它代表了笔的笔触形状,如果我们把一支铅笔笔尖削成方形的,那么画出来的线条会是一条弯曲的“矩形”,这就是笔触的意思。提供以下三种样式:

  • Paint.Cap.BUTT : 默认,没有线冒
  • Paint.Cap.SQUARE :方形
  • Paint.Cap.ROUND : 圆形

注意:从下面的示例看出BUTT和SQUARE的效果是一样的,但是他们的长度是不一样的,在代码中画的线的长度都是一样的,但最终呈现的长度SQUARE比BUTT长,这就说明,线冒(笔触)会是增加一定的长度。

Cap
  • setStrokeJoin(Join join) : 设置两线交界处的样式

提供三种样式:

  • Paint.Join.MITER : 默认,锐角
  • Paint.Join.BEVEL : 直线
  • Paint.Join.ROUND:圆弧
Join
  • setDither(boolean dither) : 设置是否使用图片抖动处理,会使绘制的图片更加清晰和颜色更加的饱满

文本相关

  • measureText(text,start,end) : 测量文本的宽度
  • breakText(text,start,end,,boolean measureForwards, float maxWidth, float[] measuredWidth) :测量文本的宽度

measureForwards :是否从前往后测量
maxWidth:最大的测量宽度,一般给个较大的值即可。
measuredWidth:该参数是一个数据,接受返回的测量结果

这两个API都是测量文本的宽度,经测试,测量结果是一样的

  • getTextBounds(String text, int start, int end, Rect bounds) : 测量文本的矩形区域,通过该API可以获取文本的高度

    Rect rect = new Rect();
    mPaint.getTextBounds(mText,0,mText.length(),rect);
    int textHeight = rect.height();//获取测量文本的高度
    int textWidth = rect.width();//测量的宽度有误差
    

注意:虽然getTextBounds也可以获取文本的宽度,但是测量的宽度结果是有误差的。后面的示例会说明这一点。

在View正中间绘制文本

第一种方式:

绘制文本跟绘制其他的图形是不同的,canvas绘制文本是从左下角开始绘制的,而并不是左上角,所以要想让文本显示在View的正中间,就必须正确的计算出左下角的坐标(相对于View本身)。如下图所示:我们先假设View的宽度为viewWidth,高度为viewHeight;文本的宽度为textWidth,高度为textHeight,如果文本想居于正中间位置,那么它们之间的关系为:
x = (viewWidth - textWidth) / 2
y = (viewHeight + textHeight) / 2
所以我们只要知道View的宽高和文本的宽高就可以准确的计算出左下角的坐标(x,y),而View的宽高是在布局中指定的,我们直接获取即可,文本的宽高我们用上述的API即可获取。

 @Override
 protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    float textWidth = mPaint.measureText(mText, 0, mText.length());//测量文本宽度
    Rect rect = new Rect();
    mPaint.getTextBounds(mText,0,mText.length(),rect);//测量文本区域,获取高度
    int textHeight = rect.height();
    //计算做下角的坐标
    float x = (width - textWidth) * 1.0f / 2;
    float y = (height + textHeight) * 1.0f / 2;
    mPaint.setColor(Color.parseColor("#FF4081"));
    canvas.drawCircle(width * 1.0f / 2,height * 1.0f / 2,width * 1.0f / 2 ,mPaint);//绘制背景
    mPaint.setColor(Color.parseColor("#FFFFFF"));
    canvas.drawText(mText,x,y,mPaint);//绘制文本,左下角开始
    //绘制中心轴线,便于观察文本是否居中
    canvas.drawLine(0,height * 1.0f / 2,width,height * 1.0f / 2,mPaint);
    canvas.drawLine(width * 1.0f / 2,0,width * 1.0f / 2,height,mPaint);
}
效果图

注意:如果使用getTextBounds来获取文本的宽度,是有误差的,如下图所示:

第二种方式:

drawText(String text, float x, float y, Paint paint),该方法中的y指的是文本基线(baseline)的纵坐标,何为基线?其实基线并不真实存在,只是一种抽象的概念。超过基线向下的部分称为降部(descent);基线向上到文本的最顶端称为升部(ascent);要注意的是:基线向下的值为正值,向上的值为负值。Paint 中提供了相应的API来获取这些值。

基线
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
// 负值
float top = fontMetrics.top; 
float ascent = fontMetrics.ascent;
//正值
float bottom = fontMetrics.bottom; 
float descent = fontMetrics.descent;

结合上图,假设我们让文本居于正中间,灰色线位置,那么我们只要求对应的基线位置即可。根据图中标注所示,各个尺寸之间的关系为:
baselineY = centerY + a的绝对高度
a = (bottom - top) / 2 - bottom // 这是减去top是应为top的值为负值,其实就是取top得绝对值。
这样我们就能求出基线的位置了。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPaint.setColor(Color.parseColor("#FF4081"));
    canvas.drawCircle(width * 1.0f / 2,height * 1.0f / 2,width * 1.0f / 2 ,mPaint);//绘制背景
    //绘制中心轴线,便于观察文本是否居中
    mPaint.setColor(Color.parseColor("#FFFFFF"));
    canvas.drawLine(0,height * 1.0f / 2,width,height * 1.0f / 2,mPaint);
    canvas.drawLine(width * 1.0f / 2,0,width * 1.0f / 2,height,mPaint);
    float textWidth = mPaint.measureText(mText, 0, mText.length());//测量文本宽度
    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
    float bottom = fontMetrics.bottom;
    float top = fontMetrics.top;
    float x = (width - textWidth) / 2;
    float y = height * 1.0f / 2 + ((bottom - top) / 2 - bottom );
    canvas.drawText(mText,x,y,mPaint);//绘制文本
}
效果图

图像相关

图像处理

其实就是不同的颜色矩阵对图像的处理效果

图像分析-RGBA模型

R:红色;G:绿色;B:蓝色;A:透明度 (Android中使用ARGB(透明度在前))
色相/色调:物体传递的颜色(RGB)
饱和度:颜色的纯度,从0(灰)到100%来描述
亮度/明度:颜色的相对明暗程度

//设置图片的色调,饱和度和亮度
public static Bitmap handleImageEffect(Bitmap bm, float hue, float saturation, float lum) {
  //Android不能对图像直接处理,我们需要创建图像的副本对其操作 
  Bitmap bmp = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas =   new Canvas(bmp);//创建一个和传出的图像一样大的画布 
  Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);//创建一个画笔 
  //设置色调
  ColorMatrix hueMatrix = new ColorMatrix();
  hueMatrix.setRotate(0, hue); //0:代表红色
  hueMatrix.setRotate(1, hue); 
   hueMatrix.setRotate(2, hue); 
   //设置饱和度
  ColorMatrix saturationMatrix = new ColorMatrix();
  saturationMatrix.setSaturation(saturation); 
  //设置亮度 
  ColorMatrix lumMatrix = new ColorMatrix(); 
  lumMatrix.setScale(lum, lum, lum, 1); 
  //将以上设置的参数集合起来 
  ColorMatrix imageMatrix = new ColorMatrix();
  imageMatrix.postConcat(hueMatrix);
  imageMatrix.postConcat(saturationMatrix); 
  imageMatrix.postConcat(lumMatrix); 
  //将设置的参数传递给画笔 
  paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
  //使用我们设置好的画笔将传入的图像绘制在画布上
  canvas.drawBitmap(bm, 0, 0, paint); 
  return bmp; 
} 

switch (seekBar.getId()) { 
  case R.id.seekbarHue: //经验值,这样的效果最好 
  mHue = (progress - MID_VALUE) * 1.0F / MID_VALUE * 180; 
  break;
  case R.id.seekbarSaturation: //经验值,这样的效果最好 
  mStauration = progress * 1.0F / MID_VALUE; 
  break; 
  case R.id.seekbatLum: //经验值,这样的效果最好
   mLum = progress * 1.0F / MID_VALUE; 
  break; 
} 
  mImageView.setImageBitmap(ImageHelper.handleImageEffect(bitmap, mHue, mStauration, mLum));
  方法内部改变的还是颜色矩阵

颜色矩阵

初始化颜色矩阵是不改变ARGB的值得
颜色矩阵中的第五列代表颜色的偏移量:改变相应的值就会改变相应的颜色的比重
颜色矩阵分量:每个像素颜色的表示

//通过改变颜色矩阵来改变图片的颜色
private void setImageMatrix() {
    Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
            Bitmap.Config.ARGB_8888);
    android.graphics.ColorMatrix colorMatrix = new android.graphics.ColorMatrix();
    //设置颜色矩阵
    colorMatrix.set(mColorMatrix); //一维数组,大小:20

    Canvas canvas = new Canvas(bmp);
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
    canvas.drawBitmap(bitmap, 0, 0, paint);
    mImageView.setImageBitmap(bmp);
}

画笔风格

Android定义:先绘制的图片称为Dst,后绘制的图片称为Src

//禁止硬件加速,因为Xfermode的很多属性不支持硬件加速,会显示不出想要的效果
setLayerType(LAYER_TYPE_SOFTWARE, null);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
mOut = Bitmap.createBitmap(mBitmap.getWidth(),
        mBitmap.getHeight(),
        Bitmap.Config.ARGB_8888);
//将副本图片关联到画布
Canvas canvas = new Canvas(mOut);
//抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// Android中将在画布上先画的图片定义为:Dst
//根据Xfermode中要想将一个图片变为圆角矩形,采用SrcIn模式:在下面先画一个圆角矩形,再在上面画一个传入
//的图片,两个图片混合就可以将传入的图片变为圆角矩形了
//绘制圆角矩形
canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(),
        50, 50, mPaint);
//设置Xfermode的模式
//PorterDuff.Mode:内部的计算公式为:[Sa*Da ,Sc*Da] :a:代表A通道(透明度通道) c:代表颜色通道
//如果传入的图片不支持A通道,设置是不起作用的
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// 后画的定义为:Src
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
mPaint.setXfermode(null);
//BitmapShader的使用
@Override protected void onDraw(Canvas canvas) { 
  mPaint = new Paint(); 
  mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); 
  // Shader.TileMode有三种模式:重复,拉伸,镜像 
  mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);         
  mPaint.setShader(mBitmapShader); 
  //在画布上画一个圆,内部用传入的图片填充
  canvas.drawCircle(300, 200, 200, mPaint); 
}

//使用线性渐变和矩阵来实现镜像的效果
 private void initView() {
 mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); 
 Matrix matrix = new Matrix();
 //在矩阵的缩放中将坐标的缩放大小不变,只改变y坐标的符号就可以实现将图片倒置的效果     
 matrix.setScale(1, -1); 
 mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0, mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true); 
 mPaint = new Paint(); 
//设置线性渐变 mPaint.setShader(new LinearGradient(0, mSrcBitmap.getHeight(), 0, mSrcBitmap.getHeight() * 1.4F, 0XDD000000, 0X10000000, Shader.TileMode.CLAMP));
//设置Xfermode模式
 mPaint.setXfermode(
   new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); 
 } 

@Override protected void onDraw(Canvas canvas) { 
  canvas.drawColor(Color.BLACK); canvas.drawBitmap(mSrcBitmap, 0, 0, null);         
  canvas.drawBitmap(mRefBitmap, 0, mSrcBitmap.getHeight(), null);
  //绘制一个矩形,实现图片的镜像效果 
  canvas.drawRect(0, mRefBitmap.getHeight(), mRefBitmap.getWidth(), mRefBitmap.getHeight() * 2, mPaint);
}

关注微信公众号获取更多相关资源

Android小先生
Top