Android-绘图机制总结

前言

早在今年年初就发过关于Android绘图机制的博文,只不过不是在简书,是在CSDN。而且已经过去很久了有的地方也忘记了,最重要的是当时刚刚开始写博客,很多地方不规范,自己查阅起来也比较麻烦,所以重新在简书上整理一篇绘图机制的总结,内容是整合了android群英传和android开发艺术探索两本书中的内容。觉得不错的话可以点击一个“喜欢”。

这里你将了解到以下内容:

  • Android屏幕相关知识
  • Android绘图技巧
  • Android图像处理技巧
  • SurfaceView的使用

1、屏幕尺寸信息

1.屏幕参数

一块屏幕通常具备以下的几个参数
屏幕大小:
指屏幕对角线的长度,通常用寸来表示,例如4.7寸,5.5寸
分辨率:
分辨率是指实际屏幕的像素点个数,例如720X1280就是指屏幕的分辨率,宽有720个像素点,高有1280个像素点
PPI
每英寸像素又称为DPI,他是由对角线的的像素点数除以屏幕的大小所得,通常有400PPI就已经很6了

2.系统屏幕密度

这里写图片描述

3.独立像素密度dp

这是由于各种屏幕密度的不同,导致同样像素大小的长度,在不同密度的屏幕上显示长度不同,因此相同长度的屏幕,高密度的屏幕包含更多的像素点,在安卓系统中使用mdpi密度值为160的屏幕作为标准,在这个屏幕上,1px = 1dp,其他屏幕则可以通过比例进行换算,例如同样是100dp的长度,mdpi中为100px,而在hdpi中为150,我们也可以得出在各个密度值中的换算公式,在mdpi中 1dp = 1px, 在hdpi中, 1dp = 1.5px,在xhdpi中,1dp = 2px,在xxhdpi中1dp = 3px,由此可见,我们换算公式l:m:h:xh:xxh = 3:4:6:8:12

4.换算公式

这里就不贴出来了,点击跳转到我历史博客即可;
系统提供TypedValue帮助我们转换

/**
     * dp2px
     * @param dp
     * @return
     */
    protected int dp2px(int dp){
        return (int)
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources()
.getDisplayMetrics());
    }

    /**
     * sp2px
     * @param dp
     * @return
     */
    protected int sp2px(int sp){
        return (int)
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp
,getResources().getDisplayMetrics());
    }

二、绘图基础

1.Paint

作为一个非常重要的元素,功能也是很强大的以下是一些基础的属性和对应的功能

  • setAntiAlias(); //设置画笔的锯齿效果
  • setColor(); //设置画笔的颜色
  • setARGB(); //设置画笔的A、R、G、B值
  • setAlpha(); //设置画笔的Alpha值
  • setTextSize(); //设置字体的尺寸
  • setStyle(); //设置画笔的风格(空心或实心)
  • setStrokeWidth(); //设置空心边框的宽度
  • getColor(); //获取画笔的颜色

接下来实现一个Demo
代码:

        paint1 = new Paint();
        paint1.setColor(Color.BLUE);
        paint1.setAntiAlias(true);
        //空心
        paint1.setStyle(Paint.Style.STROKE);
        paint2 = new Paint();
        paint2.setColor(Color.GREEN);
        //实心
        paint2.setStyle(Paint.Style.FILL);

效果图:


这里写图片描述

2.Canvas

四种常用的方法

        canvas.save();          //保存画布
        canvas.restore();       //合并图层
        canvas.translate(x,y);  //画布平移到x,y处
        canvas.rotate(翻转的角度,圆心的X坐标,圆心的Y坐标);//画布翻转

DrawPoint 绘制点
canvas.drawPoint(x,y,paint);

DrawLine绘制直线
canvas.drawLine(startX,startY,endX,endY,paint);
绘制多条直线:
float[] pts1={startX1,startY1,endX1,endY1...startXn,startYnendXn,endYn};
canvas.drawLines(pts,paint);

DrawRect绘制矩形
canvas.drawRect(left,top,right,button,paint);

DrawRoundRect绘制圆角矩形
canvas.drawRect(left,top,right,button,radiusX,radiusY,paint);

DrawCircle绘制圆
canvas.drawCircle(圆心X坐标,Y坐标,半径,paint1);

DrawArc绘制弧形/扇形
两种写法:
第一种:canvas.drawArc(left,top,right,button, startAngle, sweepAngle, useCenter, paint2);
第二种:
RectF rectF = new RectF(left,top,right,button);
canvas.drawArc(rectF, 0, 100, true, paint2);
在这里rectF代表的是圆弧外轮扩矩形区域
startAngle表示起始的角度 (以X轴正方向为0度顺时针开始算)
sweepAngle为圆弧扫过的角度
useCenter设置为true的时候显示圆心

这里写图片描述

DrawPath绘制路径
绘制多边形

        //实例化路径
        Path path = new Path();
        path.moveTo(80, 200);// 此点为多边形的起点
        path.lineTo(120, 250);
        path.lineTo(80, 250);
        path.lineTo(300, 300);
        //path.close(); // 使这些点构成封闭的多边形
        canvas.drawPath(path, paint1);

绘制曲线:

        Path path=new Path();
        path.moveTo(100, 320);          //设置Path的起点
        path.quadTo(160,320,180,410);   //设置路径点和终点
        canvas.drawPath(path, paint1);


DrawOval绘制椭圆
DrawOval(left,top,right,button,paint);

RectF rectF = new RectF(210,100,250,130) ;
 canvas.drawOval(rectF, paint2) ;

DrawText绘制文本
canvas.drawText("自定义文本",X坐标,Y坐标,paint);
绘制不同位置的文本信息

float [] pts=new float[]{100,100,200,200,300,300,400,400,500,500,600,600};
        canvas.drawPosText("text",pts,paint1);

效果图:


不同位置绘制文本信息

三、XML绘图

1、Bitmap

在XML中使用Bitmap很简单

<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@mipmap/ic_launcher">
</bitmap>

通过这样引用图片就可以将图片直接转化成Bitmap让我们在程序中使用

2、Shape

通过Shape可以绘制各种图形,下面展示一下shape的参数

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!--默认是rectangle-->
    <!--当shape= rectangle的时候使用-->
    <corners
        android:bottomLeftRadius="1dp"
        android:bottomRightRadius="1dp"
        android:radius="1dp"
        android:topLeftRadius="1dp"
        android:topRightRadius="1dp" />
    <!--半径,会被后面的单个半径属性覆盖,默认是1dp-->

    <!--渐变-->
    <gradient
        android:angle="1dp"
        android:centerColor="@color/colorAccent"
        android:centerX="1dp"
        android:centerY="1dp"
        android:gradientRadius="1dp"
        android:startColor="@color/colorAccent"
        android:type="linear"
        android:useLevel="true" />

    <!--内间距-->
    <padding
        android:bottom="1dp"
        android:left="1dp"
        android:right="1dp"
        android:top="1dp" />

    <!--大小,主要用于imageview用于scaletype-->
    <size
        android:width="1dp"
        android:height="1dp" />

    <!--填充颜色-->
    <solid android:color="@color/colorAccent" />

    <!--指定边框-->
    <stroke
        android:width="1dp"
        android:color="@color/colorAccent" />
    <!--虚线宽度-->
    android:dashWidth= "1dp"

    <!--虚线间隔宽度-->
    android:dashGap= "1dp"
</shape>

3.Layer

Layer是在PhotoShop中是非常常用的功能,在Android中,我们同样可以实现图层的效果

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!--图片1-->
    <item android:drawable="@mipmap/ic_launcher"/>

    <!--图片2-->
    <item
        android:bottom="10dp"
        android:top="10dp"
        android:right="10dp"
        android:left="10dp"
        android:drawable="@mipmap/ic_launcher"
        />
</layer-list>

4.Selector

Selector的作用是帮助开发者实现静态View的反馈,通过设置不同的属性呈现不同的效果

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 默认时候的背景-->
    <item android:drawable="@mipmap/ic_launcher" />

    <!-- 没有焦点时候的背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_window_focused="false" />

    <!-- 非触摸模式下获得焦点并点击时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" android:state_pressed="true" android:state_window_focused="true" />

    <!-- 触摸模式下获得焦点并点击时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" android:state_focused="false" android:state_pressed="true" />

    <!--选中时的图片背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_selected="true" />

    <!--获得焦点时的图片背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_focused="true" />

</selector>

这一方法可以帮助开发者迅速制作View的反馈,通过配置不同的触发事件,selector会自动选中不同的图片,特别是自定义button的时候,而我们不再使用原生单调的背景,而是使用selector特别制作的背景,就能完美实现触摸反馈了

通常情况下,上面提到的这些方法都可以共同实现,下面这个例子就展示了在一个selector中使用shape作为他的item的例子,实现一个具体点击反馈效果的,圆角矩形的selector,代码如下

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <!--填充颜色-->
            <solid android:color="#33444444" />
            <!--设置按钮的四个角为弧形-->
            <corners android:radius="5dp" />
            <!--间距-->
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>

    <item>
        <shape android:shape="rectangle">
            <!--填充颜色-->
            <solid android:color="#FFFFFF" />
            <!--设置按钮的四个角为弧形-->
            <corners android:radius="5dp" />
            <!--间距-->
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>
</selector>

四、Android绘图技巧

在学完Android的基本绘图之后我们来讲解一下常用的绘图技巧

1、Canvas

  • Canvas.save()
  • Canvas.restore()
  • Canvas.translate()
  • Canvas.roate()
    首先,我们来看一下前面两个方法
    在讲解这两个方法之前,首先来了解一下Android绘图的坐标体系,这个其实这个前面已经讲了,这里不赘述,而Canvas.save()这个方法,从字面上的意思可以理解为保存画布,他的作用就是讲之前的图像保存起来,让后续的操作能像在新的画布一样操作,这跟PS的图层基本差不多

而Canvas.restore()这个方法,则可以理解为合并图层,,就是讲之前保存下来的东西合并.
8
而后面两个方法尼?从字母上理解画布平移或者旋转,但是把他理解为坐标旋转更加形象,前面说了,我们绘制的时候默认坐标点事左上角的起始点,那么我们调用translate(x,y)之后,则将原点(0,0)移动到(x,y)之后的所有绘图都是在这一点上执行的

2.Layer图层

Android中的绘图API,很大程度上都来自绘图的API,特别是借鉴了很多PS的原理,比如图层的概念,相信看过图层的也都知道是个什么样的,我们画个图来分析一下

Android通过saveLayer()方法,saveLayerAlpha()将一个图层入栈,使用restore()方法,restoreToCount()方法将一个图层出栈,入栈的时候,后面的所有才做都是发生在这个图层上的,而出栈的时候,则会把图层绘制在上层Canvas上,我们仿照API Demo来

@Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(150, 150, 100, mPaint);

        canvas.saveLayerAlpha(0, 0,400,400,127,LAYER_TYPE_NONE);
        mPaint.setColor(Color.RED);
        canvas.drawCircle(200, 200, 100, mPaint);
        canvas.restore();
    }

当绘制两个相交的圆时,就是图层
接下来将图层后面的透明度设置成0-255不同值
我们分别演示 127 255 0三个


五.Android图像处理之画笔特效处理

不管是在我们的世界里,还是在Android的世界里,要想画出好的图片,就必须账务画笔的特效,今天我们就来玩玩这个Paint的特殊Get

1.PorterDuffXfermode

image.png

途中列举了16种PorterDuffXfermode,有点像数学中的集合,主要是一个混合显示模式

这里注意了,PorterDuffXfermode设置两个图层交集区域的显示方法des是先画的图像,src是后画的

当然,这些模式也不是经常的用到,用到最多的事,使用一张图片作为另一张图片的遮罩,通过控制遮罩层的图形,来控制下面被遮罩的显示效果,其中最常用的就是通过DST_IN.SRC_IN模式来实现将一个矩形变成圆角图片的效果,我们这里来实现一下

mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.nice);
        mOut = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mOut);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(), 80, 80, mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(mBitmap,0,0,mPaint);

下面我们来做一个比较类似于刮刮卡的效果,其实效果就是两张图片,上面那张一刮就显示下面的那张看,这个效果同样我们可以使用PorterDuffXfermode去实现,我们首先要做的就是初始化一些数据

Join
Cap
public class PlayView extends View {

    private Bitmap mBgBitmap, mFgBitmap;
    private Paint mPaint;
    private Canvas mCanvas;
    private Path mPath;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public PlayView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mPaint = new Paint();
        mPaint.setAlpha(0);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);//让笔更加圆滑一些
        mPaint.setStrokeWidth(50);
        mPaint.setStrokeCap(Paint.Cap.ROUND);//让笔更加圆滑一些

        mPath = new Path();
        mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.nice);
        mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(), mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mFgBitmap);
        mCanvas.drawColor(Color.GRAY);
    }

    /**
     * 触摸事件
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.reset();
                mPath.moveTo(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_UP:
                mPath.lineTo(event.getX(), event.getY());
                break;
        }
        mCanvas.drawPath(mPath, mPaint);
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBgBitmap, 0, 0, null);
        canvas.drawBitmap(mFgBitmap, 0, 0, null);
    }
}

2,Shader

Shader又被称为着色器。渲染器,它可以实现渲染,渐变等效果,Android中的Shader包括以下几种

BitmapShader:位图Shader
LinearGradient:线性Shader
RadialGradient:光束Shader
SweepGradient:梯形Shader
ComposeShader:混合Shader

除了第一个之外,其他的都是比较正常的,实现了名副其实的渐变,渲染效果,而与其他Shader的效果不同的是,BitmapShader所产生的是一个图像,有点类似PS里面的图像填充,他的作用是通过Paint对画布进行制定bitmap的填充,填充式有一下几个模式供选择。

  • CLAMP拉伸——拉伸的是图片最后的哪一个像素,不断重复
  • REPEAT重复——横向,纵向不断重复
  • MIRROR镜像——横向不断翻转重复,纵向不断翻转重复

这几种模式的含义都非常好理解的,与字面意识基本相同,这里最常用的是CLAMP拉伸模式,虽然他会拉伸最后一个像素,但是只要将突破设置成一个固定的像素,是可以避免的,

六.View之孪生兄弟---SurfaceView

Android系统提供了VieW进行绘图处理, vieW可以满足大部分的绘图需求,但在某些时却也有些心有余而力不足,特别是在进行一些开发的时候。 我们知道,VieW通过刷新来视图, Android系统通过发出VSYNC信号来进行屏幕的重绘, 刷新的间隔时间为I6ms。在16ms内View完成了你所需要执行的所有操作,那么用户在视觉上, 就不会产生卡顿的感觉:而如果执行的操作逻辑太多,特别是需要频繁刷新的界面上,例如游戏界面,那么就塞主线程,从而导致画面卡顿,很多时候,在自定义VieW的Log中经常会看见如下示的警告

Skipped 47 frames! The application may be doing too much work on its main thread

这些警告的产生,很多情况下就是因为在绘制过程中, 处理逻辑太多造成的为了避免这一问题的产生一 Android系统提供了surfacevicW组件来解决这个问题,可以说是VieW的孪生兄弟,但它与View还是有所不同的,它们的区别主要体现

  1. View主要用于自动更新的情况下,而surfaceVicw主要适用于被动更新,例如频繁刷新
  2. View在主线程中刷新,而surfaceView通常会通过一 个子线程来进行页面刷新。
  3. View在绘制的时候没有双缓冲机制,而surfaceVicw在底层实现机制中就已经实现了双缓冲机制;

总结起来,如果你的自定义View需要频繁刷新,或者刷新时数据处理量比较大,那么你就可以考虑使用surfaceVicw取代View了

1.surfaceView的使用

一套模板代码:

public class SurfaView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用于绘制的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private boolean mIsDrawing;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public SurfaView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
      //在这里不断绘制要绘制的内容
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            //这里要不断地用mCanvas方法绘制
        } catch (Exception e) {

        } finally {
            if (mCanvas != null) {
                //提交
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

注意

@Override
public void run() {
long start = System.currentTimeMillis();
while (mIsDrawing) {
draw();
}
long end = System.currentTimeMillis();
// 50 - 100
if (end - start < 100) {
try {
Thread.sleep(100 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
由于不断的调用draw()来进行绘制,有时候也不用那么频繁。可以设置Sleep操作。尽可能的省资源
100ms是一个经验值,通常可以设置成50-100ms.

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

推荐阅读更多精彩内容