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

概述:

很多时候我们想要自己写一些类似时钟、罗盘的控件,却又找不到合适的Demo。我想这时你可能索性就直接上图片了。在Android有Canvas和Paint这么好的画师的情况下,还是选择使用图片,的确是有一些尴尬了。下面我就利用一步一步实现自定义时钟来对这个问题做一个讲解。(注明:本人不太会制作GIF图片,以下图片均不能动态展示,想要查看动态效果,请转到博客末尾处下载源码进行查看)

错误示例:

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

效果图展示:

 

看了上面的两张运行效果图我们可以看到很正常的两张运行图,不过这不是全部。错误信息下面再进行展示和分析。在这里我就来解释一下为什么说这个示例不是全错,只是不恰当的原因。因为如果我们的需求是不用变化的图形,例如一些多边形的展示等,不需要实时去刷新界面,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)本例中,使用的是全局刷新。

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

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

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

实例示范:

运行效果图展示:

 

看到以上的运行效果图是不感觉很弦?写出来的时候,我也感觉比用图片实现的要好很多。接下来就来慢慢学习一下实现它的过程吧。

首先要做的事

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》进行学习,或以评论的方式与我进行交流。

时间: 2024-10-25 05:47:35

Android UI编程进阶——使用SurfaceViewt和Canvas实现动态时钟的相关文章

Android UI编程之自定义控件初步(下)——CustomEditText

概述: 基于对上一篇博客<Android UI编程之自定义控件初步(上)--ImageButton>的学习,我们对自定义控件也有了一个初步的认识.那现在我们可以再试着对EditText进行一些自定义的学习.以下有两种方式的自定义UI编程分享给大家. 示例:带删除按钮的输入框 效果图展示:   基本雏形搭建: 大家可以从上面的效果图上看到两个东西:左侧的EditText和右侧的图片(这里是一个Button).我们在EditText中的输入为空的时候,不显示右侧的清除按钮.一旦EditText中输

Android UI组件进阶(2)——仿Windows对话框

Android UI组件进阶(2)--仿Windows对话框 在开始本章前先祝大家中秋节快乐哈,相信很多上班的朋友都是放三天假的哈! 有时间的话回家陪陪父母吧!树欲静而风不止,子欲养而亲不待!岁月不饶人! 好了,道理和祝福语就说到这里了,今天给大家准备的是模仿Windows风格对话框! 效果图: 相信大部分的AlertDialog都是下面这个样子的: 今天给大家讲解的对话框是下面这样的: 对比两种对话框,站在用户的角度,相信你更加钟情于第二种颜色鲜明的对话框 好了下面就开始讲解如何制作模仿win

Android UI编程之自定义控件初步——ImageButton

概述: 我想我们在使用一些App的时候,应该不会出现一些"裸控件"的吧.除非是一些系统中的软件,那是为了保持风格的一致性,做出的一些权衡.我这里并非是在指责Android原生的控件不好看,说实在的,我很喜欢Android的一些原生控件.只是有些时候为了风格的一致性,就不得不去花些功夫在美工上.这于美工这一点,我对某讯的产品的确欣赏.下面就让我们开始一点一点学习Android UI编程中的自定义控件. 分析: 自定义控件就点像堆积木,并给它涂上颜色,和功能说明.下面就让我们用一个例子来逐

Android UI编程(6)——HandlerThread

介绍: HandlerThread继承Thread,当线程开启时,也就是它run方法运行起来后,线程同时创建了一个含有消息队列的Looper,并对外提供自己这个Looper对象的get方法,这就是它和普通Thread唯一不同的地方. 好处: 为什么要使用HandlerThread 1.开发中如果多次使用类似new Thread(){}.start(); 这种方式开启一个子线程,会创建多个匿名线程,使得程序运行越来越慢,而HandlerThread自带Looper使他可以通过消息来多次重复使用当前

Android UI组件进阶(1)——带进度条的按钮

Android UI组件进阶(1)--带进度条的按钮 本节引言: 这个系列是继Android UI组件实例大全后的进阶系列,在该系列中我们将进一步的学习 Android UI组件,建议阅读本系列前线学习下UI组件实例大全系列,掌握基本组件的使用; 当然你也可以直接学习本系列!好了,废话不多说,直接开始第一节吧!本节要演示的是: 带进度条的按钮!相信大家在360手机助手到看到这个东东吧: 本节要实现的就是下方这个点击后显示进度的按钮 效果图: 必备基础: 1.进度条的一些属性: backgroun

Android UI编程(4)——Thread、Message、Handler

当应用程序启动时,会开启一个主线程(也就是UI线程),由它来管理UI,监听用户点击,来响应用户并分发事件等.所有一般在主线程中不要执行比较耗时的操作,如延时.下载网络数据.死循环,否则出现ANR错误.所以就将这些操作放在子线程中,但是由于Android UI线程是不安全的,所有只能在主线程中更新UI.使用Thread来创建子线程.使用Message来存储数据.使用Handler来处理消息数据. 总结: 1.子线程与UI主线程之间通过Message来传递数据,需要创建一个新类(MyHandler)

Android UI编程(7)——Fragment

Fragment是Activity的界面中的一部分或一种行为.你可以把多个Fragment们组合到一个Activity中来创建一个多面界面并且可以在多个Activity中重用一个Fragment.也可以把Fragment认为模块化的一段Activity,它具有自己的生命周期,接收它自己的事件,并可以在Activity运行时被添加或删除. Fragment不能独立存在,它必须嵌入到activity中,而且Fragment的生命周期直接受所在的Activity的影响.例如:当Activity暂停时,

Android UI编程(5)——Looper

Looper通常是运行在一个消息的循环队列中的这个线程中,线程默认不会提供一个循环的消息去关联它们,即在一般的线程中是没有一个消息队列去关联这个消息的.那么如果线程想管理这些消息,就必须在此线程中调用Looper.prepare()使这个消息队列运行起来,并且调用Looper.loop()这个方法使它消息队列一直运行到停止.而Handler就是消息队列一个交互消息,包括从将消息发到消息队列,以及从消息队列取出消息并处理. 总结: Android使用Message对象来管理消息数据,并将这些Mes

Android UI编程(View、ViewGroup类、按钮、TextView、EditText)

1.View和ViewGroup类 Android中所有的UI元素都是使用View和ViewGroup类的对象建立的. View:将一些信息绘制在屏幕上可以与用户产生交互 Viewgroup:包含多个View和Viewgroup的容器,用来定义UI布局 2.按钮 (1)方式一: 配置: <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height=