• Android UI编程进阶——使用SurfaceViewt和Canvas实现动态时钟


    概述:

        很多时候我们想要自己写一些类似时钟、罗盘的控件,却又找不到合适的Demo。我想这时你可能索性就直接上图片了。在Android有Canvas和Paint这么好的画师的情况下,还是选择使用图片,的确是有一些尴尬了。下面我就利用一步一步实现自定义时钟来对这个问题做一个讲解。


    错误示例:

        这里我有一个“错误”的示例。这里的错误其实应该是要打上双引号的,因为它不是真的错误,只是在某些时候,它是不适当的。下面就让我们先来学习一下这个示例,了解一下这个示例中哪些是不适合使用的技术。


    效果图展示:

     

    看了上面的两张运行效果图我们可以看到很正常的两张运行图,不过这不是全部。错误信息下面再进行展示和分析。在这里我就来解释一下为什么说这个示例不是全错,只是不恰当的原因。因为如果我们的需求是不用变化的图形,例如一些多边形的展示等,不需要实时去刷新界面,OK,这个示例没有任何问题,而且使用简单。针对这一点,我想也是有必要附上代码来展示一下实现过程。


    静态画图代码:

    public class CustomCanvasView extends View {
    
        private static final String TAG = CustomCanvasView.class.getName();
        private Paint paint;
        
        private int mRadius;
    
        private Canvas mCanvas;
        
        private int mHours;
        private int mMinutes;
        private int mSeconds;
        
        private Thread mThread;
        
        public CustomCanvasView(Context context, int radius) {
            super(context);
            paint = new Paint();
            paint.setColor(Color.RED);
            paint.setStrokeJoin(Paint.Join.ROUND);
            paint.setStrokeCap(Paint.Cap.ROUND);
            paint.setStrokeWidth(5);
            
            mRadius = radius;
        }
    
        // 在这里我们将测试canvas提供的绘制图形方法
        @Override
        protected void onDraw(Canvas canvas) {
            mCanvas = canvas;
            
            drawCompass(mCanvas);
            
            refreshClock();
        }
    
        private void refreshClock() {
            mThread = new Thread() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            handler.sendEmptyMessage(0x123);
                            sleep(1000);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            mThread.start();
        }
        
        Handler handler = new Handler() {
            public void handleMessage(Message msg) {
                Calendar c = Calendar.getInstance();
                mHours = c.getTime().getHours();
                mMinutes = c.getTime().getMinutes();
                mSeconds = c.getTime().getSeconds();
                
                invalidate();
                
                c = null;
            };
        };
        
        /**
         * 绘制罗盘
         * 2015-2-3
         */
        private void drawCompass(Canvas canvas) {
            paint.setAntiAlias(true);
            paint.setStyle(Style.STROKE);
            canvas.translate(canvas.getWidth() / 2, mRadius + 300); // 平移罗盘
            canvas.drawCircle(0, 0, mRadius, paint); // 画圆圈
    
            // 使用path绘制路径文字
            canvas.save();
            
            drawLabel(canvas);
            
            canvas.restore();
    
            drawDividing(canvas);
    
            drawMinuteHand(canvas, 0);
            
            canvas = null;
        }
        
        /**
         * 绘制罗盘内侧的标签文本
         * 2015-2-4
         */
        private void drawLabel(Canvas canvas) {
            canvas.translate(-155, -155);
            Path path = new Path();
            path.addArc(new RectF(0, 0, mRadius + 100, mRadius + 100), -180, 180);
            Paint citePaint = new Paint(paint);
            citePaint.setTextSize(30);
            citePaint.setStrokeWidth(1);
            canvas.drawTextOnPath("http://blog.csdn.net/lemon_tree", path, 35, 0, citePaint);
            
            path = null;
            citePaint = null;
            canvas = null;
        }
        
        /**
         * 绘制刻度
         * 2015-2-4
         */
        private void drawDividing(Canvas canvas) {
            Paint divdPaint = new Paint(paint); // 小刻度画笔对象
            divdPaint.setStrokeWidth(1);
            divdPaint.setTextSize(20);
            
            float y = mRadius;
            int count = 60; // 总刻度数
    
            canvas.rotate(35 * 360 / count, 0f, 0f);
            
            for (int i = 0; i < count; i++) {
                if (i % 5 == 0) {
                    canvas.drawLine(0f, y, 0, y + 20f, paint);
                    canvas.drawText(String.valueOf(i / 5 + 1), -4f, y + 55f, divdPaint);
                } else {
                    canvas.drawLine(0f, y, 0f, y + 15f, divdPaint);
                }
                canvas.rotate(360 / count, 0f, 0f); // 旋转画纸
            }
            
            divdPaint = null;
            canvas = null;
        }
        
        /**
         * 绘制分针
         * 2015-2-4
         */
        private void drawMinuteHand(Canvas canvas, int second) {
            Paint tmpPaint = new Paint(paint);
            tmpPaint.setStrokeWidth(2);
            tmpPaint.setTextSize(30);
            
            tmpPaint.setColor(Color.GRAY);
            tmpPaint.setStrokeWidth(4);
            canvas.drawCircle(0, 0, 10, tmpPaint);
            tmpPaint.setStyle(Style.FILL);
            tmpPaint.setColor(Color.YELLOW);
            
            canvas.drawCircle(0, 0, 5, tmpPaint);
            
            canvas.rotate(mSeconds * 6, 0f, 0f);
            canvas.drawLine(0, 20, 0, -135, paint);
            
            tmpPaint = null;
            canvas = null;
        }
    }


    错误日志展示及原因分析:

    说实话上面两张图看上去真的很棒,可是如果你下载了我的源码并运行之后,你可能就会发现,在你的指针走了大概20秒的时候,程序就挂了。查看日志就会发现如下错误信息:


    是不是有一种又是该死的OOM问题的感觉,说实话我也是这种感觉。这可能是因为invalidate()的时候没有清理回收资源的问题,而且这里的自定义控件是继承View,没有采用双缓冲技术,致使程序崩溃。而此处的资源回收我也做了一些努力,可是问题依旧存在。于是我就开始找寻另一条路径来解决问题——SurfaceView。

    ----------------------------------------- Split -------------------------------------------

    正确示例:

    前导知识学习——脏矩形:

        所谓脏矩形刷新,意为仅刷新有新变化的部分所在的矩形区域,而其他没用的部分就不去刷新,以此来减少资源浪费。我们可以通过在获取Canvas画布时,为其指派一个参数来声明我们需要画布哪个局部,这样就可以只获得这个部分的控制权.(参考来自:http://www.linuxidc.com/Linux/2012-02/54367.htm)本例中,使用的是全局刷新。

    前导知识学习——双缓冲:

        关于双缓冲的概念,这里引用一下百度百科的说明(点击进入)。

        如果要按照我的理解来通俗地讲一遍的话,我想应该是这个样子的:有一个暗房,里面有一个功能深厚的画师,他负责绘制图画。暗房对外提供了一个小窗口,这个小窗口是用来展示画师画出来的图画。这个暗房里还有一个画师的助理,他负责把画师画出来的图画以一定速度展示在这个小窗口上(这边的一定速度肯定是比画师绘画的速度要慢一些)。


    实例示范:

    运行效果图展示:

     Custom-Clock

    看到以上的运行效果图是不感觉很炫?(注:上面的GIF看上去有些怪,那是因为本人截图没截好的原因,运行程序的时候会流畅很多)写出来的时候,我也感觉比用图片实现的要好很多。接下来就来慢慢学习一下实现它的过程吧。


    首先要做的事

    1.extends SurfaceView

    2.implements SurfaceHolder.Callback

    3.自定义一个Thread


    第二步:逻辑功能实现

    基于上一个不恰当的版本,这里对上面的逻辑功能进行一些引用。

    绘制秒针:

    /**
         * 绘制秒针
         * 2015-2-4
         */
        private void drawSecondHand(Canvas canvas) {
            Paint handPaint = new Paint(mPaint);
            handPaint.setStrokeWidth(2);
            handPaint.setStyle(Style.FILL);
    
            int angle = (mSeconds + 25) * 6; // 计算角度
            canvas.rotate(angle, 0f, 0f);
            canvas.drawLine(0, 20, 0, -135, mPaint);
        }

    绘制分针:

    /**
         * 绘制分针
         * 2015-2-4
         */
        private void drawMinuteHand(Canvas canvas) {
            Paint handPaint = new Paint(mPaint);
            handPaint.setStrokeWidth(2);
            handPaint.setStyle(Style.FILL);
    
            canvas.save();
            int angle = (mMinutes + 25) * 6; // 计算角度
            canvas.rotate(angle, 0f, 0f);
            canvas.drawLine(0, 20, 0, -110, mPaint);
            canvas.restore();
        }
    从秒针到分针代码明显多了几行,而这多出来的几行代码有什么作用呢?

    在绘制分针的时候我们可以看到这样一句:canvas.rotate(angle, 0f, 0f);它的作用是将画布旋转angle度,而如果我们在绘制分针的时候不对画布作一个状态保存,那下次在绘制时针的时候将是旋转之后所做的逻辑,为了避免这些不必要的麻烦,我们需要对其先保存后再复原处理。


    绘制时针

    /**
         * 绘制时针
         * 2015-2-4
         */
        private void drawHourHand(Canvas canvas) {
            Paint handPaint = new Paint(mPaint);
            handPaint.setStyle(Style.FILL);
            handPaint.setStrokeWidth(8);
    
            canvas.save();
            int angle = (((mHours % 12) * 5 + 25) * 6) + (mMinutes * 6 * 5 / 60); // 计算角度
            canvas.rotate(angle, 0f, 0f);
            canvas.drawLine(0, 20, 0, -90, handPaint);
            canvas.restore();
        }
    时针的绘制和分针几乎一致,唯一要注意的是绘制时针时角度的计算。如果你这里只按小时数来计算,那它永远都是指向大刻度。永远不会指向两个大刻度之间的部分,为了解决这个问题,我们需要加上分钟数一起计算。即加了n分钟下时针又偏移了多少角度。

    自定义Thread

    使用SurfaceView需要用到一个锁的机制。也就是说我这边绘图的时候,不允许被打扰,有一个独占的概念。可以通过以下代码实现:

    class DrawThread extends Thread {
            private SurfaceHolder holder;
            public boolean isRun;
            
            public DrawThread(SurfaceHolder holder) {
                this.holder = holder;
                isRun = true;
            }
            
            @Override
            public void run() {
    
                while (isRun) {
                    Canvas canvas = null;
                    try {
                        synchronized (holder) {
                            canvas = holder.lockCanvas(null);
                            canvas.drawColor(Color.BLACK);
                            drawClock(canvas);
                            holder.unlockCanvasAndPost(canvas); // 解锁画布,提交画好的图像
                            
                            Thread.sleep(1000);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    大家可以看到在我上完锁之后,对画布有一行canvas.drawColor(Color.BLACK);的代码操作。我想你应该是明白为什么的。对!就是清屏!如果没有这一句代码,上一次绘制的图形没有被清除,这让整个界面感觉起来很凌乱。下面就让我们一起来感受一下在没有清屏且只有一根指针的情况下,Canvas动态绘制出来的图形。

      
    大家可以明显看到时钟内侧的那一行Label,白色的部分在一点一点地加深,这就有力地说明了是因为上一次图形的残留导致的。


    好了,利用SurfaceView和Canvas对自定义时钟的学习就到这里了,如果你还有一些不太明白的地方,欢迎前往我的上一篇博客《Android自定义控件前导基础知识学习(一)——Canvas》进行学习,或以评论的方式与我进行交流。



  • 相关阅读:
    JAVA类型转换的那些坑儿
    记录一次 ajaxSubmit()提交表单
    遍历Map
    整理JS对数组的内置操作函数(转)
    html页面监听事件(转)
    js控制文本框只能输入中文、英文、数字与指定特殊符号(引用)
    target属性 和 重定向 response.sendRedirect("");
    js各种正则表达式(引用)
    前端---js定义函数的方式
    Web Service 根据wsdl调用代码
  • 原文地址:https://www.cnblogs.com/fengju/p/6336114.html
Copyright © 2020-2023  润新知