Android从零开搞系列:自定义View(3)Canvas基本API+综合应用+开源分析

来源:互联网 发布:杭州淘宝化妆师招聘 编辑:程序博客网 时间:2024/06/06 16:28

转载请注意:http://blog.csdn.net/wjzj000/article/details/52424496

本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…
https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的优秀框架于一体,全部拆离不含任何额外的库导入)
https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)


一个温度旋转按钮

先看效果:

这里写图片描述


先声明这个源码不是我写的。源码来源:
http://www.jianshu.com/p/2f7bfe1d7345?utm_campaign=haruki&utm_content=note&utm_medium=reader_share&utm_source=qq
我只是把这个项目源码拆分出来,记录其中涉及到的自定义View的一些用法。


当然,如果直接上手分析源码,我觉得有点对不起题目中最基本的自定义语法。所以先来瞅瞅相关的api。当然这里不涉及到onDraw(),onMeasure(),onLayout()的知识点。如果不是很清楚可以看我的另一篇与此相关的博客。

这个时候,我相信大家应该已经知道,天朝也可以访问Android官网了…当然是.cn后缀(https://developer.android.google.cn/index.html)
不过这里还要安利一个我常用的源码查看网站,(估计以后也用不到了,默默纪念一下):
http://androiddoc.qiniudn.com/reference/packages.html
一色的英文我有点受不了!没关系,我们直接上谷歌翻译(谷歌翻译是不用翻墙就可以使用的,功能时非常的diao。为了翻译方便大家可以装到浏览器的插件中)


让我们一起走进基础语法

首先,我们自定义View的时候,必然要继承与父类。然后通过重写特定的方法,然后通过画布(Canvas)调用相关的API来实现我们想要的效果。

让我们先看一下Canvas的官方介绍:
The Canvas class holds the “draw” calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).
翻译一下:Canvas类持有“画”的回调。要绘制的东西,你需要4个基本组成部分:一个持有像素的位图,画布主办的绘制调用(写入位图),绘图基础(如矩形,路径,文本,位图)和涂料(以描述绘制的颜色和样式)。


Canvas中有一系列的公共方法:

//这里仅展示部分,还有很多同类重载的方法boolean clipPath(Path path)相交指定的路径当前剪辑。boolean clipRect(RectF rect)相交指定的矩形,这是在局部坐标系表示的当前剪辑。boolean clipRect(float left, float top, float right, float bottom)相交指定的矩形,这是在局部坐标系表示的当前剪辑。void    drawARGB(int a, int r, int g, int b)装满指定的ARGB颜色整个画布“位图(仅限于当前剪辑),使用srcover porterduff模式。void    drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)绘制与指定弧,这将是缩放到适合指定的椭圆形内。void    drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)绘制使用指定的矩阵的位图。void    drawCircle(float cx, float cy, float radius, Paint paint)绘制使用指定的油漆指定的圆。void    drawColor(int color)填充以指定的颜色整个画布“位图(仅限于当前剪辑),使用srcover porterduff模式。void    drawLine(float startX, float startY, float stopX, float stopY, Paint paint)绘制指定的开始线段和停止x,y坐标,使用指定的油漆。void    drawOval(float left, float top, float right, float bottom, Paint paint)绘制使用指定的涂料指定椭圆形。

补充:画布的宽高等于这个控件的宽高
补充:画布的(0,0)在画布的左上角
下文是对这些API简单的使用。有详细注释!


看一个简单的API实例,注释有详解

  • 先看一下效果
    这里写图片描述
  • onDraw中的代码
@Override    protected void onDraw(Canvas canvas) {        /**         * Paint有含一个参数的构造方法,接受int值。但这个int不是用于设置Color的!         * 这是来自官方的翻译:创建一个具有指定的标志一个新的油漆。使用setFlags()更改创建这些油漆后。         * 例如参数:UNDERLINE_TEXT_FLAG  涂料适用下划线装饰绘制文本标志。         * 所以这种写法是有误的:Paint paint=new Paint(Color.BLACK)         * 画笔的默认颜色是黑色,而不是通过构造函数设置进去的!!         */        //初始化一个画笔对象。        Paint paint=new Paint();        //给画笔设置为黑色        paint.setColor(Color.BLACK);        //设置画笔实线宽度为2dp        paint.setStrokeWidth(dp2px(2));        //设置画笔写出的字体大小16sp        paint.setTextSize(sp2px(26));        //保存画布状态(此时保存的状态为未对画布进行任何操作)        canvas.save();        /**         *顺旋转画布20度,此时的旋转是在保存之后         *大家可以理解成,我们人拿着画布旋转了20度,然后在此基础上画图         */        //!!!画布的旋转,坐标系也会跟着转!!!想象自己画画时,把白纸转动的那种情况        canvas.rotate(20);        /**         * 在canvas这块画布用paint画笔;         * 以(100,100)为起点,(200,100)为终点;         * 用drawLine方法画线。         */        canvas.drawLine(100,100,400,100,paint);        /**         * 恢复画布状态,此后的代码仍会在一个未进行过任何操作的画布上。         * 即:没有刚才rotate方法的旋转操作。         */        canvas.restore();        canvas.drawLine(100,400,400,400,paint);        canvas.save();        /**         * 将画笔的模式设置为不填充。         * 什么意思呢?就是正常我们使用笔芯为2dp的画笔去画任何东西,         * 默认情况是填充模式,也就是画什么东西我们都要用这个2dp的         * 画笔反复涂抹,直到全部涂满图形。         * 不过设置成这样呢?那就是不涂抹,也就是空心字的效果。         */        paint.setStyle(Paint.Style.STROKE);        //在(100,400)坐标的位置写字,注意看效果:这个坐标是第一个字符的左下角的位置。        canvas.drawText("Hahahaha",100,400,paint);        //将画笔的模式设置为填充,那么我们的矩形就会被涂满。        paint.setStyle(Paint.Style.FILL);        //new Rect(100,500,300,600)的意思就是生成一个起点(100,500)终点(300,600)的区域        //此方法和canvas.drawRect(100,500,300,600,paint)效果一样        canvas.drawRect(new Rect(100,500,300,600),paint);    }

好的,关于基本的API先介绍到此,其实用法都是大同小异。不过需要终点理解save和restore方法,以及画布的旋转移动的。

如果对Canvas的旋转平移还是不能很好的理解,可以参考我的另一篇博客:
http://blog.csdn.net/wjzj000/article/details/54925149


在开始分析源码的之前,我们先拆分一下这个效果

  • 最外边有一层由横线汇成的弧线,弧线的起始和结束位置显示数字
  • 往里来,是一个弧线弧线的弧度对应最外层
  • 然后再往里,是一个课旋转的按钮,作者使用的是一个图片
  • 最里边是温度度数的显示
  • 最下边有一行文字“最高温度设置”

好的,分析完构造,让我们一步步来分析并记录这里边涉及的自定义View的API以及知识点。

最外层效果实现

这里写图片描述

private void drawScale(Canvas canvas) {        //先保存画布状态,因为我们最终的效果有很多层面,而且每一层的画布都相对是独立的        canvas.save();        //移动画布到屏幕的中心位置(0,0)也被移动。就是将画布整体移动。        //!!!注意,画布的宽高等于这个控件的宽高。默认的(0,0)是它自身的左上角!!!        canvas.translate(getWidth() / 2, getHeight() / 2);        /**         * 逆时针旋转135-2度         * 此处为什么要旋转?         * 因为我们正常画的时候圆的时候它从x为0,y为半径处起笔,然后         * 顺时针画至闭合。而我们想要的最终效果,就需要正常效果逆时针         * 旋转即可。         */        canvas.rotate(-133);        dialPaint.setColor(Color.parseColor("#3CB7EA"));        /**         * 这里就是画刻度的循环,这里有点绕,不过静下心也比较简单。         * 我们要有一个概念。画布的绘图的原理和我们画图是一个样。         * 大家可以这样想象,左手操控画布,右手拿着笔。         * 最开始时,我们通过左手逆时针旋转了我们的画布133度;         * 但是我们的右手,不受画布的影响,该什么画还怎么画。         * 在这个画刻度的过程。我们的右手画笔只做一个动作:画线         * canvas.drawLine(0, -dialRadius, 0, -dialRadius + scaleHeight, dialPaint)         * 注意我们canvas.translate()方法,已经将画布移至屏幕中间;         * 所以此时的(0,0)就是屏幕中心点。         * 咱们再回到右手上来,右手一直都是在重复的画直线。为什么有弧线的效果?         * 因为左手控制画布在旋转!!!         */        for (int i = 0; i < 60; i++) {            //这里y为什么是负的?因为在安卓的坐标系里,y轴原点往下是正;            //x轴往左是负            canvas.drawLine(0, -dialRadius, 0, -dialRadius + scaleHeight, dialPaint);            canvas.rotate(4.5f);        }        /**         * 如果这里大家不理解为什么又转了90,那这就尴尬了。         * 咱们为了这个效果,最初,逆时针转了130+度;         * 然后又顺时针转了60乘4.5度,也就是说,画布回正了140度左右(就是数学计算题)         * 而我们想让画布重回-130+度,那么再顺势针转90即可达到效果。         * !!因为要保证蓝色刻度和红色刻度的起始点相同!!         * 当然我们也可以再逆时针回270度:效果是一样的         * 也就是canvas.rotate(-270);         * 当然还有一个方法。那就是我们重新恢复画布,然后在逆时针转133度:         * canvas.restore();         * canvas.rotate(-133);         * 当然我们还要先把画布移到中心,不然它会画到左上角去。         */        canvas.rotate(90);        dialPaint.setColor(Color.parseColor("#E37364"));        for (int i = 0; i < (temperature - minTemp) * angleRate; i++) {            canvas.drawLine(0, -dialRadius, 0, -dialRadius + scaleHeight, dialPaint);            canvas.rotate(4.5f);        }        //此效果完成,恢复画布状态。        canvas.restore();    }

第一层效果我们就完成了,接下来看第二层!


往里一层效果实现

这里写图片描述

有了刚才的基础,那么这个效果就非常的简单了。
旋转画布,然后画弧线。妥妥的,就是这样~

private void drawArc(Canvas canvas) {        canvas.save();        canvas.translate(getWidth() / 2, getHeight() / 2);        canvas.rotate(135 + 2);        RectF rectF = new RectF(-arcRadius, -arcRadius, arcRadius, arcRadius);        //这个方法就是画弧线的方法,在rectF这个矩形内,画一个圆心角265度的弧(应该是这么描述吧--!)        canvas.drawArc(rectF, 0, 265, false, arcPaint);        canvas.restore();    }

这个效果比较简单就不多做累述了。


绘制文字的效果

这里写图片描述

private void drawText(Canvas canvas) {        canvas.save();        //此方法是Paint的方法,传入一个字符串获取字符串宽度。        float titleWidth = titlePaint.measureText(title);        /**         * 绘制标题"最高温度设置"         * 说实话看到这我有点方...因为作者前两部操作都是先移动画布到中心,         * 然后再搞事情。这次不移的话就是直接以左上角为(0,0)坐标。         * 咱们也可以用移画布的方式做,效果是一样的如下:         * canvas.restore();         * float titleWidth = titlePaint.measureText(title);         * canvas.drawText(title, - titleWidth / 2, dialRadius * 2 + dp2px(15), titlePaint);         */        //不同的是以不同的(0,0)坐标点所以Text的起点坐标也发生了变化。仅此而已        //width:控件的宽度。        canvas.drawText(title, (width-titleWidth)/2, dialRadius*2+dp2px(5), titlePaint);        // 绘制最小温度标识        // 最小温度如果小于10,显示为0x        String minTempFlag = minTemp < 10 ? "0" + minTemp : minTemp + "";        float tempFlagWidth = titlePaint.measureText(maxTemp + "");        //这个旋转画布的重载方法是指,以(width/2,height/2)这个坐标为原点旋转        canvas.rotate(55, width/2, height/2 );        canvas.drawText(minTempFlag, (width - tempFlagWidth) / 2, height + dp2px(5), tempFlagPaint);        // 绘制最大温度标识        canvas.rotate(-105, width / 2, height / 2);        canvas.drawText(maxTemp + "", (width - tempFlagWidth) / 2, height + dp2px(5), tempFlagPaint);        canvas.restore();    }

文字部分OK。

接下来进入中间按钮层

这里写图片描述

作者在这里直接用的一张图,所以相对比较简单。

    // 按钮图片    private Bitmap buttonImage = BitmapFactory.decodeResource(getResources(),            R.mipmap.btn_rotate);    // 按钮图片阴影    private Bitmap buttonImageShadow = BitmapFactory.decodeResource(getResources(),            R.mipmap.btn_rotate_shadow);    // 抗锯齿    private PaintFlagsDrawFilter paintFlagsDrawFilter;    private void drawButton(Canvas canvas) {        // 按钮宽高        int buttonWidth = buttonImage.getWidth();        int buttonHeight = buttonImage.getHeight();        // 按钮阴影宽高        int buttonShadowWidth = buttonImageShadow.getWidth();        int buttonShadowHeight = buttonImageShadow.getHeight();        // 绘制按钮阴影        canvas.drawBitmap(buttonImageShadow, (width - buttonShadowWidth) / 2,                (height - buttonShadowHeight) / 2, buttonPaint);        //Matrix(矩阵),用于操作Bitmap对象,比如移动旋转等        //关于矩阵的变换,请往下看...        Matrix matrix = new Matrix();        // 设置按钮位置        matrix.setTranslate(buttonWidth / 2, buttonHeight / 2);        // 设置旋转角度        matrix.preRotate(45 + rotateAngle);        // 按钮位置还原,此时按钮位置在左上角        matrix.preTranslate(-buttonWidth / 2, -buttonHeight / 2);        // 将按钮移到中心位置        matrix.postTranslate((width - buttonWidth) / 2, (height - buttonHeight) / 2);        //设置抗锯齿        canvas.setDrawFilter(paintFlagsDrawFilter);        canvas.drawBitmap(buttonImage, matrix, buttonPaint);    }

关于矩阵的变换:http://blog.csdn.net/wjzj000/article/details/53524646
这不是一篇纯讲矩阵变换的,但是博客的中下部分有所涉及。


最后一步,在按钮上绘制温度文字

这里写图片描述

那这个效果就更简单了,就是纯粹的文字绘制。

private void drawTemp(Canvas canvas) {        canvas.save();        canvas.translate(getWidth() / 2, getHeight() / 2);        float tempWidth = tempPaint.measureText(temperature + "");        float tempHeight = (tempPaint.ascent() + tempPaint.descent()) / 2;        canvas.drawText(temperature + "°", -tempWidth / 2 - dp2px(5), -tempHeight, tempPaint);        canvas.restore();    }

然而,到这就完了么?还没有了,这一系列步骤完毕后。我们仅仅是完成了静态的效果,别忘了咱们还要转动呢!既然有动作,那么我们肯定要分析它的onTouchEvent()

这里整体的思路比较清晰。代码如下

@Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                isDown = true;                float downX = event.getX();                float downY = event.getY();                currentAngle = calcAngle(downX, downY);                break;            case MotionEvent.ACTION_MOVE:                isMove = true;                float targetX;                float targetY;                downX = targetX = event.getX();                downY = targetY = event.getY();                float angle = calcAngle(targetX, targetY);                // 滑过的角度增量                float angleIncreased = angle - currentAngle;                // 防止越界                if (angleIncreased < -270) {                    angleIncreased = angleIncreased + 360;                } else if (angleIncreased > 270) {                    angleIncreased = angleIncreased - 360;                }                IncreaseAngle(angleIncreased);                currentAngle = angle;                invalidate();                break;            case MotionEvent.ACTION_CANCEL:            case MotionEvent.ACTION_UP: {                if (isDown && isMove) {                    // 纠正指针位置                    rotateAngle = (float) ((temperature - minTemp) * angleRate * 4.5);                    invalidate();                    // 回调温度改变监听                    onTempChangeListener.change(temperature);                    isDown = false;                    isMove = false;                }                break;            }        }        return true;    }

相关设计的方法

/**     * 以按钮圆心为坐标圆点,建立坐标系,求出(targetX, targetY)坐标与x轴的夹角     *     * @param targetX x坐标     * @param targetY y坐标     * @return (targetX, targetY)坐标与x轴的夹角     */    //作者在这里写了很多数学上的计算,本质是为了得到一个。得到一个0-360度的手势滑动效果。    /* 思路是求出一圈的弧度,然后通过弧度制换成角度制     * 当然我们可能都知道1弧度大约等于57度,360度约等于6.2弧度。     * 但是计算机不知道啊,所以要想办法整出来。     * 作者使用了这个函数:Math.atan()这个函数要传递一个tan的值然后返回对应的弧度。     * 根据三角函数,tan的取值是正无穷到负无穷。当tan为负数时弧度也会为负,所以这不是我们想要的结果。作者在这里让tan始终为正,然后通过一系列的加减做出了弧度0-6.2的效果。     * 以下是作者的解决方案。(如果不好理解可以看一下我的方案)     * /    private float calcAngle(float targetX, float targetY) {        float x = targetX - width / 2;        float y = targetY - height / 2;        double radian;        if (x != 0) {            float tan = Math.abs(y / x);            if (x > 0) {                if (y >= 0) {                    radian = Math.atan(tan);                } else {                    radian = 2 * Math.PI - Math.atan(tan);                }            } else {                if (y >= 0) {                    radian = Math.PI - Math.atan(tan);                } else {                    radian = Math.PI + Math.atan(tan);                }            }        } else {            if (y > 0) {                radian = Math.PI / 2;            } else {                radian = -Math.PI / 2;            }        }        //此公式是将弧度换算为角度。        return (float) ((radian * 180) / Math.PI);    }    //以下是我的解决方案    private float test(float targetX, float targetY){        float x = targetX - width / 2;        float y = targetY - height / 2;        double radian;        float angle;        //此方法相当于传入y/x就是tan        radian=Math.atan2(y,x);        //因为tan有正有负,所以这里得到的angle是0-180度以及0-负180度        angle= (float) ((180*radian)/Math.PI);        if (angle<0){            //0-负180度就是对应的180-360,所以在这里进行简单的转换            angle=360+angle;        }        //angle即为0-360        return angle;    }    /**     * 增加旋转角度     *     * @param angle 增加的角度     */    private void IncreaseAngle(float angle) {        rotateAngle += angle;        if (rotateAngle < 0) {            rotateAngle = 0;        } else if (rotateAngle > 270) {            rotateAngle = 270;        }        temperature = (int) (rotateAngle / 4.5) / angleRate + minTemp;    }

以及那俩个dp,sp转px的工具类

public int dp2px(float dp) {        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,                getResources().getDisplayMetrics());    }private int sp2px(float sp) {        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,        getResources().getDisplayMetrics());}

尾声

OK,文章到这就算是分析结束了。希望对各种Androider们在自定义View中有所帮助。

最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp

3 0