6.4 Android绘图技巧(Primary:Canvas & Layer, 附demo-仪表盘、圆形头像、裁剪动画绘制)

1.Canvas的变换方法

  • Canvas.save()
    这个方法从字面上可以理解为保存画布,
    调用时,将当前的画布(canvas)保存到Canvas栈。

  • Canvas.restore()
    Canvas栈弹栈,取出栈顶的canvas作为当前的canvas形状。

  • Canvas.restoreToCount(int saveCount)
    不断弹栈,直到弹出索引是saveCount的栈顶canvas;

save()、restore()、restoreToCount()是对应着有一个画布栈的,
调用save()时候将当前的画布(canvas)入Canvas栈,
同时返回一个入栈后在栈中的索引;
restore()出栈;

这里关于Canvas的保存和恢复的三个方法,笔者写了一个demo,由于篇幅有限,放在另外一篇博客里面Canvas的保存和恢复的demo,欢迎各位小伙伴前往惠读指教~


  • Canvas.translate()
    Android默认绘图坐标零点位于屏幕左上角,那么在调用translate()之后,则将零点(0,0)移动到了(x,y)。之后所有绘图操作都将以(x,y)为原点执行。

  • Canvas.rotate()
    与translate()同理,旋转坐标系一个一定的角度。

  • Canvas.scale()

  • Canvas.skew()

  • canvas.clip()
    clip函数根据传入的Rect、Path、Region来获得最新的画布形状;

2.Demo:仪表盘

2.1.画外圆

2.2.画刻度和刻度值

2.3.画指针

2.4.全代码和运行结果

Clock.java:

package com.yishengxu.myapplication;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class Clock extends View {

    private int mHeight, mWidth;

    public Clock(Context context) {
        super(context);
    }

    public Clock(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Clock(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 获取宽高参数
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        // 画外圆
        Paint paintCircle = new Paint();
        paintCircle.setStyle(Paint.Style.STROKE);
        paintCircle.setAntiAlias(true);
        paintCircle.setStrokeWidth(5);
        canvas.drawCircle(mWidth / 2,
                mHeight / 2, mWidth / 2, paintCircle);
        // 画刻度
        Paint painDegree = new Paint();
        paintCircle.setStrokeWidth(3);
        for (int i = 0; i < 24; i++) {
            // 区分整点与非整点
            if (i == 0 || i == 6 || i == 12 || i == 18) {
                painDegree.setStrokeWidth(5);
                painDegree.setTextSize(30);
                canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,//基线起点x
                        mWidth / 2, mHeight / 2 - mWidth / 2 + 60,//基线起点y
                        painDegree);
                String degree = String.valueOf(i);//Integer.toString(i)
                canvas.drawText(degree,
                        mWidth / 2 - painDegree.measureText(degree) / 2,//measureText()在画布上输出文本之前,检查字体的宽度:
                        mHeight / 2 - mWidth / 2 + 90,
                        painDegree);
            } else {
                painDegree.setStrokeWidth(3);
                painDegree.setTextSize(15);
                canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
                        mWidth / 2, mHeight / 2 - mWidth / 2 + 30,
                        painDegree);
                String degree = String.valueOf(i);
                canvas.drawText(degree,
                        mWidth / 2 - painDegree.measureText(degree) / 2,
                        mHeight / 2 - mWidth / 2 + 60,
                        painDegree);
            }
            // 通过旋转画布简化坐标运算
            canvas.rotate(15, mWidth / 2, mHeight / 2);//二三参数为枢轴点的xy,枢轴点即旋转中心
        }
        // 画圆心
        Paint paintPointer = new Paint();
        paintPointer.setStrokeWidth(30);
        canvas.drawPoint(mWidth / 2, mHeight / 2, paintPointer);
        // 画指针
        Paint paintHour = new Paint();
        paintHour.setStrokeWidth(20);
        Paint paintMinute = new Paint();
        paintMinute.setStrokeWidth(10);
        canvas.save();//只是保存“缓冲区”绘制的内容
        canvas.translate(mWidth / 2, mHeight / 2);
        canvas.drawLine(0, 0, 100, 100, paintHour);
        canvas.drawLine(0, 0, 100, 200, paintMinute);
        canvas.restore();//将“缓冲区”绘制的内容和已经save()的内容一同合并并保存起来,这里跟上边的save注意区分开来
    }
}

MainActivity.java:

package com.yishengxu.myapplication;

import android.app.Activity;
import android.os.Bundle;


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Clock(this));
    }
}

效果图:

3.Layer图层

创建一个新的Layer到“栈”中,可以使用saveLayer(), savaLayerAlpha(), 从“栈”中推出一个Layer,可以使用restore(),restoreToCount()。但Layer入栈时,后续的DrawXXX操作都发生在这个Layer上,而Layer退栈时,就会把本层绘制的图像“绘制”到上层或是Canvas上,在复制Layer到Canvas上时,可以指定Layer的透明度

  • 透明度:
  • 127,半透明
  • 255,完全不透明
  • 0,完全透明
    实例如Demo下图:

上Demo:

package com.imooc.myapplication;

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


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new MyLayer(this));
    }

    public class MyLayer extends View {

        private Paint mPaint;
        private static final int LAYER_FLAGS =
                        Canvas.MATRIX_SAVE_FLAG |
                        Canvas.CLIP_SAVE_FLAG |
                        Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
                        Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
                        Canvas.CLIP_TO_LAYER_SAVE_FLAG;//此乃API定义的常量,ctrl+E 进入文档查看便知晓其含义

        public MyLayer(Context context) {
            super(context);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.WHITE);//背景
            mPaint.setColor(Color.BLUE);
            canvas.drawCircle(150, 150, 100, mPaint);//“零图层”

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

半透明:


完全不透明: canvas.saveLayerAlpha(0, 0, 400, 400, 255, LAYER_FLAGS);

image.png

完全透明: canvas.saveLayerAlpha(0, 0, 400, 400, 0, LAYER_FLAGS);


自定义View——圆形头像

思路:
获取一张图片的bitmap对象,
根据图片大小构造一条适宜图片大小的圆形路径,
绘图时,
保存画布,把画布裁剪成圆形,画上位图,回复画布,即可

  • 其中注意,
    为了避免选择的图片太大,
    这里使用到了图片压缩技术;

上代码

public class CustomCircleView extends View {

    private Bitmap mBmp;
    private Paint mPaint;
    private Path mPath;

    public CustomCircleView(Context context) {
        super(context);
        init();
    }

    public CustomCircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mBmp = decodeSampledBitmapFromResource(getResources(), R.drawable.testtheview, 100, 100);
        mPaint = new Paint();
        mPath = new Path();

        int width = mBmp.getWidth();
        int height = mBmp.getHeight();

        float r = (width / 2) > (height / 2) ? (height / 2) : (width / 2);

        mPath.addCircle(width / 2, height / 2, r, Path.Direction.CCW);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        canvas.clipPath(mPath);
        canvas.drawBitmap(mBmp, 0, 0, mPaint);
        canvas.restore();
    }

    
    //下面两个方法用于进行图片压缩
    public static int calculateInSampleSize(BitmapFactory.Options options,
                                            int reqWidth, int reqHeight) {
        // 源图片的高度和宽度
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            // 计算出实际宽高和目标宽高的比率
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
            // 一定都会大于等于目标的宽和高。
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                         int reqWidth, int reqHeight) {
        // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        // 调用上面定义的方法计算inSampleSize值
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 使用获取到的inSampleSize值再次解析图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }


}

MainActivity:

public class MainActivity extends AppCompatActivity {

    private LinearLayout ll_nextParent;
    private LinearLayout.LayoutParams layoutParams;

    private CanvasTestView canvasTestView;
    private int canvasDrawId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //初始化控件和点击事件
        initViews();

        //为了方便调试,定义此方法,输入不同的id,显示不同的自定义View
        configCustomViews(2);
    }

    private void initViews() {

        canvasDrawId = 0;

        ll_nextParent = findViewById(R.id.ll_nextParent);

        layoutParams = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);


    }


    private void configCustomViews(int drawId) {
        switch (drawId) {
            case 0:
                //SpiderView...
                break;

            case 1:
                //canvasTestView...
                break;

            case 2:
                CustomCircleView customCircleView = new CustomCircleView(this);
                ll_nextParent.addView(customCircleView,layoutParams);
                break;

            default:
        }
    }
}
  • 通过在MainActivity.java 中设置,
    或者在activity_main.xml中添加位置属性之类等等,
    便可以设定这个圆形头像的位置;

  • 所用图片,来自百度图片

  • 效果图:


裁剪动画

  • Region并不是用来画图的,它的主要作用就是裁剪画布;

  • 原理:
    短时间内不断改变一个宽度值clipwidth
    每次改变时将裁剪区域(传给clip方法当做参数的区域Region)变大,
    在裁剪区域内的图像显示出来,
    而裁剪区域之外的图像不会显示;

  • 问题关键在于计算裁剪区域:

    裁剪区域主要是由两类矩形不断交叠而成,
    一类从左到右变大(裁剪区域一),
    另一类从右到左变大(裁剪区域二)

  • 每次重绘,
    while (i * CLIP_HEIGHT <= bitmapHeight)中把整个Bitmap画完,
    同时每次,矩形便向对应方向变大(变长)一点;

话不多说,上代码

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private LinearLayout ll_nextParent;
    private LinearLayout.LayoutParams layoutParams;

    private CanvasTestView canvasTestView;
    private int canvasDrawId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //初始化控件和点击事件
        initViews();

        //为了方便调试,定义此方法,输入不同的id,显示不同的自定义View
        configCustomViews(3);
    }

    private void initViews() {

        canvasDrawId = 0;

        ll_nextParent = findViewById(R.id.ll_nextParent);

        layoutParams = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);


    }


    private void configCustomViews(int drawId) {
        switch (drawId) {
            case 0:
                SpiderView spiderViewOri = new SpiderView(this);
                ll_nextParent.addView(spiderViewOri, layoutParams);
                break;

            case 1:
                canvasTestView = new CanvasTestView(this);
                ll_nextParent.addView(canvasTestView, layoutParams);
                break;

            case 2:
                CustomCircleView customCircleView = new CustomCircleView(this);
                ll_nextParent.addView(customCircleView,layoutParams);
                break;

            case 3:
                DisplayMetrics outMetrics = new DisplayMetrics();
                getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
                int widthPixels = outMetrics.widthPixels;
                int heightPixels = outMetrics.heightPixels;

                final ClipRgnView clipRgnView = new ClipRgnView(this);
                clipRgnView.setDecodeSize(300,400);

                clipRgnView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        clipRgnView.clipWidth = 0;
                        clipRgnView.reDraw();
                    }
                });

                ll_nextParent.addView(clipRgnView,layoutParams);
                break;

            default:
        }
    }

自定义View——ClipRgnView:

public class ClipRgnView extends View {

    private Bitmap mBitmap;
    public int clipWidth = 0;
    private int bitmapWidth;
    private int bitmapHeight;
    private static final int CLIP_HEIGHT = 30;
    //    private Region mRgn;
    private Path mPath;

    public ClipRgnView(Context context) {
        super(context);
        init();
    }

    public ClipRgnView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);

//        mRgn = new Region();
        mPath = new Path();
    }

    public void setDecodeSize(int bmpWidth, int bmpHeight) {
        mBitmap = decodeSampledBitmapFromResource(getResources(),R.drawable.testtheview,bmpWidth,bmpHeight);
        bitmapWidth = mBitmap.getWidth();
        bitmapHeight = mBitmap.getHeight();
    }

    public void reDraw() {
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

//        mRgn.setEmpty();
        mPath.reset();

        int i = 0;//花了多少个矩形区域

        while (i * CLIP_HEIGHT <= bitmapHeight) {
            if (i % 2 == 0) {
//                mRgn.union(new Rect(0, i * CLIP_HEIGHT, clipWidth, (i + 1) * CLIP_HEIGHT));
                mPath.addRect(new RectF(0, i * CLIP_HEIGHT, clipWidth,
                        (i + 1) * CLIP_HEIGHT), Path.Direction.CCW);

            } else {
//                mRgn.union(new Rect(bitmapWidth - clipWidth, i * CLIP_HEIGHT, bitmapWidth, (i + 1) * CLIP_HEIGHT));
                mPath.addRect(new RectF(bitmapWidth - clipWidth,
                        i * CLIP_HEIGHT, bitmapWidth, (i + 1) * CLIP_HEIGHT), Path.Direction.CCW);
            }
            i++;
        }

        canvas.clipPath(mPath);
        canvas.drawBitmap(mBitmap, 0, 0, new Paint());

        if (clipWidth > bitmapWidth) {
            return;
        }

        clipWidth += 5;

        postInvalidate();
    }


    //下面两个方法用于进行图片压缩
    public static int calculateInSampleSize(BitmapFactory.Options options,
                                            int reqWidth, int reqHeight) {
        // 源图片的高度和宽度
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            // 计算出实际宽高和目标宽高的比率
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
            // 一定都会大于等于目标的宽和高。
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                         int reqWidth, int reqHeight) {
        // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        // 调用上面定义的方法计算inSampleSize值
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 使用获取到的inSampleSize值再次解析图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

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

推荐阅读更多精彩内容