Android 绘制计时器
用小米的手机,发现其实还可以,无意间点开小米的计时器,发现界面非常好看和实用。于是自己仿照着写一个,由于技术不好,代码整体结构上可能有点乱,但主要的实现功能和掌握知识点。
Android中绘制采用canvase绘图类,加上timer计时器和handler来更新UI,核心就这点东西,非常简单。废话不多说,先附上效果图。
和小米自带的还是有差距的,主要是运行的时候指针的转动感觉没小米的那么流畅。这一点还在优化中。直接上代码,所有的注视都写上了,没什么好说的。
主页面xml文件
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#E0EEEE" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <LinearLayout android:id="@+id/main_view" android:layout_width="match_parent" android:layout_height="100dp" android:layout_weight="5" android:orientation="horizontal" > </LinearLayout> <View android:layout_width="match_parent" android:layout_height="1dp" android:background="#FFFFFF" /> <ListView android:id="@+id/main_listview" android:layout_width="match_parent" android:layout_height="30dp" android:layout_weight="2" > </ListView> <View android:layout_width="match_parent" android:layout_height="1dp" android:background="#FFFFFF" /> <Button android:id="@+id/main_button_start" android:layout_width="match_parent" android:layout_height="10dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_weight="1" android:text="开始" /> <LinearLayout android:id="@+id/main_lin" android:layout_width="match_parent" android:layout_height="10dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_weight="1" android:orientation="horizontal" android:visibility="gone" > <Button android:id="@+id/main_button_zanting" android:layout_width="50dp" android:layout_height="match_parent" android:layout_weight="1" android:text="暂停" /> <Button android:id="@+id/main_button_jici" android:layout_width="50dp" android:layout_height="match_parent" android:layout_weight="1" android:text="记次" /> </LinearLayout> </LinearLayout>
辅助类Times,也就是时间类,实例化即可用,然后再tostring上做了格式输出
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.timer; import java.text.Format; public class Times { public int getHour() { return Hour; } @Override public String toString() { return String.format("%02d",Hour)+":"+String.format("%02d",minute)+":"+String.format("%.1f",second); } public Times(int hour, int minute, float second) { super(); Hour = hour; this.minute = minute; this.second = second; } public void setHour(int hour) { Hour = hour; } public int getMinute() { return minute; } public void setMinute(int minute) { this.minute = minute; } public float getSecond() { return second; } public void setSecond(float second) { this.second = second; } private int Hour; private int minute; private float second; public void secondAdd() { if (second > 59) { second = 0; if (minute > 59) { minute = 0; Hour++; } else { minute++; } } else { second += 0.1; } } }
接下来是最主要的绘图代码,MyCanvases继承View,绘制圆,指针刻度等等。其中绘制指针比较复杂,但是目前没有想到什么好的方法,大家如果有,请给我分享下。指针主要是画出一个封闭的三角形,为了保持转动,使用自己写的arithmetic1一个小的算法,因为要用到圆和三角函数来计算坐标(忘了差不多了,还把数学几何图形复习了一遍......累死了。细心的朋友可能发现,Math.sin方法参数是弧度,而我显示把角度计算出来,再换算成弧度,绕了一大圈。不要喷我,当时为了这些几何图形,手头又没笔,都把我写晕了,这部分代码真心不想动了。)附上代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.timer; import java.util.Timer; import java.util.TimerTask; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathEffect; import android.os.Handler; import android.os.Message; import android.view.View; public class MyCanvases extends View { private Paint paint, paint2, paint3, paint4, paint5, paint6, paint7; private Timer timer; private Handler handler; private Canvas canvas; private float X = 50, Y = 20, W = 0, H = 0; private Times times = new Times(0, 0, 0); private Path path; Message message; MyTimer myTimer; public MyCanvases(Context context, float x, float y) { super(context); message = Message.obtain(); handler = new MyHandler(); this.W = x; this.H = y; paint = new Paint(); // 去锯齿 paint.setAntiAlias(true); paint.setColor(Color.BLACK); // 画笔样式 paint.setStyle(Paint.Style.STROKE); // 画笔粗细 paint.setStrokeWidth(1); paint3 = new Paint(); paint3.setAntiAlias(true); paint3.setColor(Color.GRAY); paint3.setStyle(Paint.Style.STROKE); paint3.setStrokeWidth(35); paint2 = new Paint(); paint2.setAntiAlias(true); paint2.setColor(Color.BLACK); paint2.setStyle(Paint.Style.FILL); paint2.setTextSize(53); paint4 = new Paint(); paint4.setAntiAlias(true); paint4.setColor(Color.rgb(224, 238, 238)); paint4.setStyle(Paint.Style.FILL_AND_STROKE); paint4.setStrokeWidth(10); paint5 = new Paint(); paint5.setAntiAlias(true); paint5.setColor(Color.YELLOW); paint5.setStyle(Paint.Style.STROKE); paint5.setStrokeWidth(30); paint6 = new Paint(); paint6.setAntiAlias(true); paint6.setColor(Color.GRAY); PathEffect pathEffect = new DashPathEffect(new float[] { 5, 5 }, 1); paint6.setPathEffect(pathEffect); paint6.setStyle(Paint.Style.STROKE); paint6.setStrokeWidth(12); paint7 = new Paint(); paint7.setAntiAlias(true); paint7.setColor(Color.GRAY); paint7.setStyle(Paint.Style.FILL); paint7.setStrokeWidth(1); } protected void onDraw(Canvas canvas) { this.canvas = canvas; // 内外圆 canvas.drawCircle(W / 2, H / 2 - 50, W / 2 - 100, paint); canvas.drawCircle(W / 2, H / 2 - 50, W / 2 - 150, paint3); // 时间 canvas.drawText(times.toString(), W / 2 - 120, H / 2 - 50, paint2); // 指针 // 这个指针其实是一个三角形,外圆上一点加上内圆上两点,构成一个闭合三脚形。时间变化时3个点一起移动,看起来就是一个指针的效果了 float a[] = arithmetic1(W / 2, H / 2 - 50, W / 2 - 137, times.getSecond()); float b[] = arithmetic1(W / 2, H / 2 - 50, W / 2 - 180, times.getSecond() - 2); float c[] = arithmetic1(W / 2, H / 2 - 50, W / 2 - 180, times.getSecond() + 2); path = new Path(); path.moveTo(a[0], a[1]); path.lineTo(b[0], b[1]); path.lineTo(c[0], c[1]); path.close(); canvas.drawPath(path, paint4); // 小圆 canvas.drawCircle(W - 150, H - 150, 80, paint); // 刻度 canvas.drawCircle(W - 150, H - 150, 70, paint6); // 指针 canvas.drawCircle(W - 150, H - 150, 10, paint7); a = arithmetic1(W - 150, H - 150, 60, times.getSecond() * 60); b = arithmetic1(W - 150, H - 150, 10, times.getSecond() * 60 - 4); c = arithmetic1(W - 150, H - 150, 10, times.getSecond() * 60 + 4); path = new Path(); path.moveTo(a[0], a[1]); path.lineTo(b[0], b[1]); path.lineTo(c[0], c[1]); path.close(); canvas.drawPath(path, paint7); } // 计时器 public class MyTimer extends TimerTask { public void run() { handler.sendMessage(new Message().obtain()); } } public class MyHandler extends Handler { public void handleMessage(Message msg) { times.secondAdd(); invalidate(); } } /** * 自定义方法,以60秒为圆的一圈,给出圆的参数,获得对应时间在圆周上点的坐标。所以不同时间可以通用,只需要根据秒对比相应的直即可。( * 例如传入的time为分钟,将time/60就是分钟的坐标变化) * * @param x * 圆心x坐标 * @param y * 圆心y坐标 * @param r * 圆的半径 * @param time * 时间 * @return 返回一个float数组,大小为2。float0为x坐标,float1为y坐标; */ public float[] arithmetic1(float x, float y, float r, float time) { float circle[] = new float[2]; float angle; angle = 90 - time * 6; circle[0] = (float) (Math.cos(angle * Math.PI / 180) * r + x); circle[1] = (float) (y - Math.sin(angle * Math.PI / 180) * r); return circle; } /** * 启动计时器 */ public void Start() { timer = new Timer(); myTimer = new MyTimer(); timer.schedule(myTimer, 0, 100); } /** * 停止计时器 */ public void Stop() { timer.cancel(); myTimer.cancel(); } /** * 获取这一刻的时间 * * @return */ public String getTime() { return times.toString(); } /** * 这个方法其实就是重置时间 * */ public void setTime() { times = new Times(0, 0, 0); invalidate(); } }
最后是主页面activity代码,其中在listview上没有写布局,就用最简单的数组资源做适配器,因为这个listview只有一个显示的功能,所以就没用多纠结。大家可以自己改改喜欢的布局加上去。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.timer; import java.util.ArrayList; import java.util.List; import android.os.Bundle; import android.app.Activity; import android.view.View; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ListView; public class MainActivity extends Activity { private ListView listview; private Button btnStart, btnJici, btnStop; private LinearLayout lin, linView; MyCanvases myCanvases; List<String> list; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); list = new ArrayList<String>(); intview(); } /** * 获得控件关联,为按钮设置监听 */ private void intview() { linView = (LinearLayout) findViewById(R.id.main_view); listview = (ListView) findViewById(R.id.main_listview); btnStart = (Button) findViewById(R.id.main_button_start); btnStart.setOnClickListener(new MyOnClick()); btnJici = (Button) findViewById(R.id.main_button_jici); btnJici.setOnClickListener(new MyOnClick()); btnStop = (Button) findViewById(R.id.main_button_zanting); btnStop.setOnClickListener(new MyOnClick()); lin = (LinearLayout) findViewById(R.id.main_lin); } /** * 实现点击事件接口,用控件id来区分不同事件,将所有事件集中到一起处理。(个人比较喜欢这样写) * * @author * */ public class MyOnClick implements OnClickListener { public void onClick(View v) { switch (v.getId()) { case R.id.main_button_start: myCanvases.Start(); btnStart.setVisibility(View.GONE); lin.setVisibility(View.VISIBLE); break; case R.id.main_button_zanting: if (btnStop.getText().toString().equals("暂停")) { myCanvases.Stop(); btnStop.setText("继续"); btnJici.setText("重置"); } else { myCanvases.Start(); btnStop.setText("暂停"); btnJici.setText("记次"); } break; case R.id.main_button_jici: if (btnJici.getText().toString().equals("重置")) { myCanvases.setTime(); btnStart.setVisibility(View.VISIBLE); lin.setVisibility(View.GONE); btnStop.setText("暂停"); btnJici.setText("记次"); // 清空数据 list.clear(); // 清空listview listview.setAdapter(null); } else { // 添加数据 list.add(myCanvases.getTime()); String[] text = new String[list.size()]; for (int i = 0; i < list.size(); i++) { text[i] = "第" + (i + 1) + "次: " + list.get(i); } // 建立适配器 ArrayAdapter<String> adapter = new ArrayAdapter<String>( MainActivity.this, android.R.layout.simple_list_item_1, text); // 添加适配器 listview.setAdapter(adapter); // 将listview的焦点始终锁定在最后一行,很有用 listview.setSelection(adapter.getCount()); } default: break; } } } // 重写activit的onWindowFocusChanged方法,是为了在布局加载完成之后, // 才能获得到控件的大小和位置,将参数传递到绘图类 public void onWindowFocusChanged(boolean hasFocus) { if (hasFocus) { myCanvases = new MyCanvases(this, linView.getWidth(), linView.getHeight()); //将绘制的图像加载到对应的布局上显示 linView.addView(myCanvases); } } }
最后说一些细节。在绘制的坐标问题上,我先是在activity布局加载完成之后,将绘制区域的宽度和高度获取到,根据这个数据来绘制,本来是想实现在不同手机屏幕实现自适应效果,后来发现。手机分辨率不一样的时候,还是没能实现,所以大家根据自己的需要改改坐标。至于怎么实现自适应屏幕,由于没有系统的学习过,现在还是没有解决好。我会努力的。还有就是小米自带计时器上有渲染的效果,就是指针转动时颜色会发生变化,这个用android自带的绘图渲染来写很麻烦。所以可能是用第三方包,这里我就不写了(试着写过,但是渲染效果很差,就删除了)。
大家有什么意见,欢迎随时交流。大神勿喷....