专栏首页Android知识点总结Android原生绘图之一起画个表

Android原生绘图之一起画个表

零、前言

前几天介绍了一大堆Android的Canvas,Paint,Path的API,接下来将是灵活地使用他们 今天带来的是一个手表的绘制,经过本篇的洗礼,相信你会对Canvas的图层概念有更深刻的理解 至于表的美丑不是本文的重点,本文只有一个目的,就是理清Canvas的save和restore的意义

表.gif


一、准备工作

1.新建类继承View
public class TolyClockView extends View {

    public TolyClockView(Context context) {
        this(context, null);
    }

    public TolyClockView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    private void init() {
        //TODO 初始化
    }
    
       @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //TODO 绘制
    }
2.分析一下

一般我们都会这样去自定义一个View,但很少人会有图层这个概念,毕竟咱都是敲代码的 如下图,一开始是一个x,y轴在顶点的图层,如果你不用save(),那你始终都在这个图层,图层栈始终只有一个

开始绘制时.png


3.下面在这个界面上绘制本人专用坐标系:(已封装成工具,附在文尾)

网格和坐标系属于辅助性的工具,绘制起来比较多,所以使用Picture录制,在init()里初始化 Picture在onDraw里绘制高效些,区别就像准备一车砖盖房子和造一块才砖盖一下房子

//成员变量
private Picture mPictureGrid;//网格Canvas元件
private Point mCoo = new Point(500, 800);//坐标系原点
private Picture mPictureCoo;//坐标系Canvas元件

//init()中
mPictureGrid = HelpDraw.getGrid(getContext());
mPictureCoo = HelpDraw.getCoo(getContext(), mCoo);
//初始化画笔
mMainPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMainPaint.setStyle(Paint.Style.STROKE);
mMainPaint.setStrokeCap(Paint.Cap.ROUND);

//onDraw里
canvas.drawPicture(mPictureGrid);
canvas.drawPicture(mPictureCoo);

正如API字面上的意思,在canvas上将网格和坐标系两张图片绘制出来,如下图:

绘制坐标系时.png


二、绘制逻辑

准备工作做好了,下面要到正题了

1.onDraw里
canvas.save();//保存先前状态(相当于在另一个图层操作)
canvas.translate(mCoo.x, mCoo.y);//将画布定点平移到绘制的坐标系中心
canvas.restore();//合并到root图层
2.看一下这两句翻译在图上是什么意思:

一旦canvas.save(),相当于新建了一个图层(黑色虚线所示), 然后canvas.translate(mCoo.x, mCoo.y)将新建的图层向右和向下移动 新建的图层的好处:只有栈顶的图层才能操作(如Canvas移动时,root图层并没有动,这正是我们想要的)

save和translate.png

3.绘制外圈破碎的圆:drawBreakCircle(canvas)
 /**
 * 绘制破碎的圆
 * @param canvas
 */
private void drawBreakCircle(Canvas canvas) {
    for (int i = 0; i < 4; i++) {
        canvas.save();//保存先前状态(相当于在另一个图层操作)
        canvas.rotate(90 * i);
        mMainPaint.setStrokeWidth(8);
        mMainPaint.setColor(Color.parseColor("#D5D5D5"));
        //在-350, -350, 350, 350的矩形区域,从10°扫70°绘制圆弧
        canvas.drawArc(
                -350, -350, 350, 350,
                10, 70, false, mMainPaint);
        canvas.restore();//恢复先前状态(相当于将图层和前一图层合并)
    }
}     

先看i=0时: 由于save了,前面的图层被锁定,相当于在另一个图层操作

绘制碎圆.png

canvas.restore()调用后, 图层2将它的结果给了图层1,挥挥衣袖,不带走一片云彩,出栈了

绘制碎圆2.png

先看i=1时: 由于save了,前面的图层被锁定,相当于在另一个图层操作 这里canvas.rotate(90 * 1)相当于当前图层转了90°,如图: 注意:我只将坐标轴的第一象限涂色,canvas图层是一个无限的面,canvas宽高只是限制显示, 旋转、平移、缩放等的关键在于坐标轴的变换,旋转90°相当于坐标轴转了90°

绘制碎圆3.png

canvas.restore()调用后, 图层2将它的结果给了图层1,挥挥衣袖,不带走一片云彩,出栈了

绘制碎圆4.png

经过这两个图层的演示,想必你应该明白图层的作用了吧。 最后画完之后,图层全合并到root

绘制碎圆5.png


4.绘制小点

画60个点(小线),每逢5变长,也就是画直线,每次将画布旋转360/60=6°

    private void drawDot(Canvas canvas) {
        for (int i = 0; i < 60; i++) {
            if (i % 5 == 0) {
                canvas.save();
                canvas.rotate(30 * i);
                mMainPaint.setStrokeWidth(8);
                mMainPaint.setColor(ColUtils.randomRGB());
                canvas.drawLine(250, 0, 300, 0, mMainPaint);

                mMainPaint.setStrokeWidth(10);
                mMainPaint.setColor(Color.BLACK);
                canvas.drawPoint(250, 0, mMainPaint);
                canvas.restore();
            } else {
                canvas.save();
                canvas.rotate(6 * i);
                mMainPaint.setStrokeWidth(4);
                mMainPaint.setColor(Color.BLUE);
                canvas.drawLine(280, 0, 300, 0, mMainPaint);
                canvas.restore();
            }
        }
    }

点绘制.png


5.绘制时针:
 /**
     * 绘制时针
     *
     * @param canvas
     */
    private void drawH(Canvas canvas) {

        canvas.save();
        canvas.rotate(40);
        mMainPaint.setColor(Color.parseColor("#8FC552"));
        mMainPaint.setStrokeCap(Paint.Cap.ROUND);
        mMainPaint.setStrokeWidth(8);
        canvas.drawLine(0, 0, 150, 0, mMainPaint);
        canvas.restore();
    }
6.绘制分针:
    /**
     * 绘制分针
     * @param canvas
     * @param deg
     */
    private void drawM(Canvas canvas) {
        canvas.save();
        canvas.rotate(120);
        mMainPaint.setColor(Color.parseColor("#87B953"));
        mMainPaint.setStrokeWidth(8);
        canvas.drawLine(0, 0, 200, 0, mMainPaint);
        mMainPaint.setColor(Color.GRAY);
        mMainPaint.setStrokeWidth(30);
        canvas.drawPoint(0, 0, mMainPaint);
        canvas.restore();
    }
7.绘制秒针
    /**
     * 绘制秒针
     *
     * @param canvas
     * @param deg
     */
    private void drawS(Canvas canvas, float deg) {
        mMainPaint.setStyle(Paint.Style.STROKE);
        mMainPaint.setColor(Color.parseColor("#6B6B6B"));
        mMainPaint.setStrokeWidth(8);
        mMainPaint.setStrokeCap(Paint.Cap.SQUARE);

        canvas.save();
        canvas.rotate(deg);

        canvas.save();
        canvas.rotate(45);
        //使用path绘制:在init里初始化一下就行了
        mMainPath.addArc(-25, -25, 25, 25, 0, 240);
        canvas.drawPath(mMainPath, mMainPaint);
        canvas.restore();

        mMainPaint.setStrokeCap(Paint.Cap.ROUND);
        canvas.drawLine(-25, 0, -50, 0, mMainPaint);

        mMainPaint.setStrokeWidth(2);
        mMainPaint.setColor(Color.BLACK);
        canvas.drawLine(0, 0, 320, 0, mMainPaint);

        mMainPaint.setStrokeWidth(15);
        mMainPaint.setColor(Color.parseColor("#8FC552"));
        canvas.drawPoint(0, 0, mMainPaint);
        canvas.restore();
    }

时针.png


8.添加文字
/**
 * 添加文字
 * @param canvas
 */
private void drawText(Canvas canvas) {
    mMainPaint.setTextSize(60);
    mMainPaint.setStrokeWidth(5);
    mMainPaint.setStyle(Paint.Style.FILL);
    mMainPaint.setTextAlign(Paint.Align.CENTER);
    mMainPaint.setColor(Color.BLUE);
    canvas.drawText("Ⅲ", 350, 30, mMainPaint);
    canvas.drawText("Ⅵ", 0, 350 + 30, mMainPaint);
    canvas.drawText("Ⅸ", -350, 30, mMainPaint);
    canvas.drawText("Ⅻ", 0, -350 + 30, mMainPaint);
    //使用外置字体放在assets目录下
    Typeface myFont = Typeface.createFromAsset(getContext().getAssets(), "CHOPS.TTF");
    mMainPaint.setTypeface(myFont);
    mMainPaint.setTextSize(70);
    canvas.drawText("Toly", 0, -150, mMainPaint);
}

效果.png

好了,静态效果实现了,现在让它动起来吧


三、让表动起来

1.显示当前时间:

表的旋转角度由每个针绘制是的canvas.rotate(XXX);决定, 那么动态改变旋转的角度不就行了吗! 看下面一道数学题:

11:12:45秒,时针、分针、秒针的指针各与中心水平线的夹角?
答:
秒针:45 / 12.f * 360 - 90
分针:12 / 60.f * 360 - 90 + 45 / 60.f * 1
时针:11 / 60.f * 360 - 90 + 12 / 60.f * 30 + 45 / 3600.f * 30
2.动态更新角度:绘制指针的三个函数,加角度参数
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int min = calendar.get(Calendar.MINUTE);
int sec = calendar.get(Calendar.SECOND);

drawS(canvas, hour / 60.f * 360 - 90 + min / 60.f * 30 + sec / 3600.f * 30);
drawM(canvas, min / 60.f * 360 - 90 + sec / 60.f);
drawH(canvas, sec / 60.f * 360 - 90);

时间.png


3.现在每次进来,都会更新时间了,怎么自动更新呢?

循环的黄金搭档:Handler + Timer

public class ClockActivity extends AppCompatActivity {

    /**
     * 新建Handler
     */
    Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            mView.invalidate();//处理:刷新视图
        }
    };

    private View mView;
    private Timer timer = new Timer();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_toly_clock);
        ButterKnife.bind(this);

        mView = findViewById(R.id.id_toly_clock);

        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(0);//发送消息
            }
        };
        //定时任务
        timer.schedule(timerTask, 0, 1000);
    }
}

表.gif

ok,完结散花(分析图画的真要命...)


后记:捷文规范

1.本文成长记录及勘误表

项目源码

日期

备注

V0.1--无

2018-11-8

Android原生绘图之一起画个表

2.更多关于我

笔名

QQ

微信

爱好

张风捷特烈

1981462002

zdl1994328

语言

我的github

我的简书

我的CSDN

个人网站

3.声明

1----本文由张风捷特烈原创,转载请注明 2----欢迎广大编程爱好者共同交流

3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正 4----看到这里,我在此感谢你的喜欢与支持


icon_wx_200.png

附录:网格+坐标系绘制工具:

1.HelpDraw
/**
 * 作者:张风捷特烈<br/>
 * 时间:2018/11/5 0005:8:43<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:辅助画布
 */
public class HelpDraw {

    /**
     * 获取屏幕尺寸
     */
    public static Point getWinSize(Context context) {
        Point point = new Point();
        Utils.loadWinSize(context, point);
        return point;
    }

    /**
     * 绘制网格
     */
    public static Picture getGrid(Context context) {
        return getGrid(getWinSize(context));
    }

    /**
     * 绘制坐标系
     */
    public static Picture getCoo(Context context, Point coo) {
        return getCoo(coo, getWinSize(context));
    }


    /**
     * 绘制网格
     *
     * @param winSize 屏幕尺寸
     */
    private static Picture getGrid(Point winSize) {

        Picture picture = new Picture();
        Canvas recording = picture.beginRecording(winSize.x, winSize.y);
        //初始化网格画笔
        Paint paint = new Paint();
        paint.setStrokeWidth(2);
        paint.setColor(Color.GRAY);
        paint.setStyle(Paint.Style.STROKE);
        //设置虚线效果new float[]{可见长度, 不可见长度},偏移值
        paint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0));
        recording.drawPath(HelpPath.gridPath(50, winSize), paint);
        return picture;

    }

    /**
     * 绘制坐标系
     *
     * @param coo     坐标系原点
     * @param winSize 屏幕尺寸
     */
    private static Picture getCoo(Point coo, Point winSize) {
        Picture picture = new Picture();
        Canvas recording = picture.beginRecording(winSize.x, winSize.y);
        //初始化网格画笔
        Paint paint = new Paint();
        paint.setStrokeWidth(4);
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        //设置虚线效果new float[]{可见长度, 不可见长度},偏移值
        paint.setPathEffect(null);

        //绘制直线
        recording.drawPath(HelpPath.cooPath(coo, winSize), paint);
        //左箭头
        recording.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y - 20, paint);
        recording.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y + 20, paint);
        //下箭头
        recording.drawLine(coo.x, winSize.y, coo.x - 20, winSize.y - 40, paint);
        recording.drawLine(coo.x, winSize.y, coo.x + 20, winSize.y - 40, paint);
        //为坐标系绘制文字
        drawText4Coo(recording, coo, winSize, paint);
        return picture;
    }

    /**
     * 为坐标系绘制文字
     *
     * @param canvas  画布
     * @param coo     坐标系原点
     * @param winSize 屏幕尺寸
     * @param paint   画笔
     */
    private static void drawText4Coo(Canvas canvas, Point coo, Point winSize, Paint paint) {
        //绘制文字
        paint.setTextSize(50);
        canvas.drawText("x", winSize.x - 60, coo.y - 40, paint);
        canvas.drawText("y", coo.x - 40, winSize.y - 60, paint);
        paint.setTextSize(25);
        //X正轴文字
        for (int i = 1; i < (winSize.x - coo.x) / 50; i++) {
            paint.setStrokeWidth(2);
            canvas.drawText(100 * i + "", coo.x - 20 + 100 * i, coo.y + 40, paint);
            paint.setStrokeWidth(5);
            canvas.drawLine(coo.x + 100 * i, coo.y, coo.x + 100 * i, coo.y - 10, paint);
        }

        //X负轴文字
        for (int i = 1; i < coo.x / 50; i++) {
            paint.setStrokeWidth(2);
            canvas.drawText(-100 * i + "", coo.x - 20 - 100 * i, coo.y + 40, paint);
            paint.setStrokeWidth(5);
            canvas.drawLine(coo.x - 100 * i, coo.y, coo.x - 100 * i, coo.y - 10, paint);
        }

        //y正轴文字
        for (int i = 1; i < (winSize.y - coo.y) / 50; i++) {
            paint.setStrokeWidth(2);
            canvas.drawText(100 * i + "", coo.x + 20, coo.y + 10 + 100 * i, paint);
            paint.setStrokeWidth(5);
            canvas.drawLine(coo.x, coo.y + 100 * i, coo.x + 10, coo.y + 100 * i, paint);
        }

        //y负轴文字
        for (int i = 1; i < coo.y / 50; i++) {
            paint.setStrokeWidth(2);
            canvas.drawText(-100 * i + "", coo.x + 20, coo.y + 10 - 100 * i, paint);
            paint.setStrokeWidth(5);
            canvas.drawLine(coo.x, coo.y - 100 * i, coo.x + 10, coo.y - 100 * i, paint);
        }
    }
}
2.HelpPath
/**
 * 作者:张风捷特烈<br/>
 * 时间:2018/11/5 0005:8:05<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:辅助分析路径
 */
public class HelpPath {

    /**
     * 绘制网格:注意只有用path才能绘制虚线
     *
     * @param step    小正方形边长
     * @param winSize 屏幕尺寸
     */
    public static Path gridPath(int step, Point winSize) {

        Path path = new Path();

        for (int i = 0; i < winSize.y / step + 1; i++) {
            path.moveTo(0, step * i);
            path.lineTo(winSize.x, step * i);
        }

        for (int i = 0; i < winSize.x / step + 1; i++) {
            path.moveTo(step * i, 0);
            path.lineTo(step * i, winSize.y);
        }
        return path;
    }

    /**
     * 坐标系路径
     *
     * @param coo     坐标点
     * @param winSize 屏幕尺寸
     * @return 坐标系路径
     */
    public static Path cooPath(Point coo, Point winSize) {
        Path path = new Path();
        //x正半轴线
        path.moveTo(coo.x, coo.y);
        path.lineTo(winSize.x, coo.y);
        //x负半轴线
        path.moveTo(coo.x, coo.y);
        path.lineTo(coo.x - winSize.x, coo.y);
        //y负半轴线
        path.moveTo(coo.x, coo.y);
        path.lineTo(coo.x, coo.y - winSize.y);
        //y负半轴线
        path.moveTo(coo.x, coo.y);
        path.lineTo(coo.x, winSize.y);
        return path;
    }
}
3.Utils
public class Utils {
    /**
     * 获得屏幕高度
     *
     * @param ctx 上下文
     * @param winSize 屏幕尺寸
     */
    public static void loadWinSize(Context ctx, Point winSize) {
        WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        if (wm != null) {
            wm.getDefaultDisplay().getMetrics(outMetrics);
        }
        winSize.x = outMetrics.widthPixels;
        winSize.y = outMetrics.heightPixels;
    }

}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android关于Paint你所知道的和不知道的一切

    张风捷特烈
  • [-Flutter 自组篇-] 蛛网图+绘制+动画实践

    张风捷特烈
  • Android关于Canvas你所知道的和不知道的一切

    Picture相当于先拍一张照片,并且是在别的Canvas上,在别的Canvas上,在别的Canvas上! 重要的话说三遍:当需要的时候在贴在当前的canva...

    张风捷特烈
  • 计算机视觉简介:历史、现状和发展趋势

    来源:中国科学院自动化所机器视觉课题组 【导读】本文由中国科学院自动化研究所模式识别国家重点实验室胡占义研究员撰写,对计算机视觉40多年的发展历程进行了简要总结...

    WZEARW
  • 利用Flask搭建微电影视频网站(一):项目介绍

    努力与运动兼备~~~有任何问题可以加我好友或者关注微信公众号,欢迎交流,我们一起进步!

    编程思录
  • ASP.NET MVC 3发布报错(ASP.NET MVC 3在没有安装环境的服务器上运行)的解决方案

    前言 ASP.NET MVC 3在没有安装MVC3的环境的服务器上运行,就会报错,缺少引用,本文就为mvc3发布报错的解决方案。 解决方案 思路:提供ASP....

    Java中文社群_老王
  • EasyExcel快速读写Excel数据

    ps: 其实本人并没有对比过POI,只是网络上资料都有这么一说,再对比了GitHub的star数,最后直接选用了EasyExcel

    十毛
  • 【破解人类识别文字之谜】对图像中的字母进行无监督学习

    【新智元导读】Nature 子刊 Nature Human Behavior 上最新发表了一篇关于人类行为的研究,通过对自然图像中的字母进行无监督学习,探讨了人...

    新智元
  • 从零开始学 Web 之 HTML(一)认识前端

    前端对于网站来说,通常是指网页,网站的前台部分包括网站的表现层和结构层。因此前端技术一般分为前端设计和前端开发。

    Daotin
  • centos7下如何将用户加入sudo组 原

    在Ubuntu下安装系统以后第一个创建的用户默认是在sudo用户组的,所以以sudo执行命令没有问题。

    拓荒者

扫码关注云+社区

领取腾讯云代金券