Android TextClock 使用及源码解析

TextClock 是Android用于显示当前时间或者日期的控件,并且支持用户自定义的日期或者时间格式。

使用

 比如我们要做一个下面效果图的界面,这里我们就可以使用TextClock来实现。

iamge

1,时间

 上面效果图中的时间,我们该怎么做呢,我们可以用一个TextClock实现

1
2
3
4
5
6
7
8
<TextClock
android:id="@+id/main_date"
style="@style/MainDateStyle"
android:layout_width="@dimen/main_date_width"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:format12Hour="@string/main_date_format12"
android:format24Hour="@string/main_date_format24"/>

其中android:format12Hour 表示12小时制格式,android:format24Hour 表示24小时制格式,对应的string资源是:

1
2
<string name="main_date_format12">h &#160;&#160;&#160;mm</string>
<string name="main_date_format24">HH &#160;&#160;mm</string>

这里是通过&#160; 来实现空格。但是这个时候就会有个问题:怎么识别不同的分辨率呢,因为中间的空格毕竟不好控制。嗯,或许你已经想到了,我们可以用两个TextClock来实现:一个表示小时,另一个表示分钟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="horizontal">
<TextClock
android:id="@+id/main_date"
style="@style/MainDateStyle"
android:layout_width="@dimen/main_date_width"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:format12Hour="h"
android:format24Hour="HH"/>
<TextClock
android:id="@+id/main_date1"
style="@style/MainDateStyle"
android:layout_width="@dimen/main_date_width"
android:layout_height="wrap_content"
android:layout_marginStart="55dip"
android:layout_marginTop="50dp"
android:format12Hour="mm"
android:format24Hour="mm"/>
</LinearLayout>

2,日期

 对应日期的使用和时间类似,主要的还是修改android:format12Hourandroid:format24Hour ,就拿上面的效果图来讲,我们可以这样实现:

1
2
3
4
5
6
7
8
9
10
<TextClock
android:id="@+id/main_time"
style="@style/MainTimeStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:format12Hour="@string/main_time_format"
android:format24Hour="@string/main_time_format"
android:paddingBottom="30dp"/>

对应的string文件定义如下:

1
2
3
4
5
<!--默认格式-->
<string name="main_time_format">yyyy/M/d &#160;&#160; EEE</string>
<!--中文格式-->
<string name="main_time_format">yyyy年M月d日&#160;&#160;EEE</string>

整个布局文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_date_time"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="horizontal">
<TextClock
android:id="@+id/main_date"
style="@style/MainDateStyle"
android:layout_width="@dimen/main_date_width"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:format12Hour="h"
android:format24Hour="HH"/>
<TextClock
android:id="@+id/main_date1"
style="@style/MainDateStyle"
android:layout_width="@dimen/main_date_width"
android:layout_height="wrap_content"
android:layout_marginStart="55dip"
android:layout_marginTop="40dp"
android:format12Hour="mm"
android:format24Hour="mm"/>
</LinearLayout>
<TextClock
android:id="@+id/main_time"
style="@style/MainTimeStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:paddingBottom="30dp"
android:format12Hour="@string/main_time_format"
android:format24Hour="@string/main_time_format"/>
</RelativeLayout>

 TextClock使用很简单,主要就是设置他的android:format12Hourandroid:format24Hour 属性。那么问题来了,改怎么设置呢?可以先参考Android开发文档中的SimpleDateFormat ,其中列出了已经定义的字母代表的含义,并且也指明了可以通过加上'' 单引号来避免被替换。比如我需要“ 2017X4X6”这样的日期格式,那么可以这样定义:yyyy'X'M'X'd ,或许你看了SimpleDateFromat文档后,你也会这样定义成YYYY'X'M'X'd ,但是你会发现你得到的结果是这样的:YYYYX4X6,为什么呢,稍后我们从源码的角度来解答。

源码分析

1,时间更新机制

 TextClock是继承自TextView,TextClock有四个构造方法,在布局文件中使用TextClock时,最终都会调用public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) 这个构造方法,所以我们从该构造方法入手,分析TextClock的原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
try {
//获取xml中定义的12小时制、24小时制、时区的格式
mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
mTimeZone = a.getString(R.styleable.TextClock_timeZone);
} finally {
a.recycle();
}
init();
}

 在上面的构造方法中调用了init()方法,该方法先会确保12小时和24小时制的格式有正确的值,然后会创建Calendar对象mTimer,该对象就是正真的时间,所定义的format都是操作mTimer,然后得到相应格式的时间、日期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void init() {
//如果在xml中没有定义12小时制或者24小时制的格式就取系统的时间格式
if (mFormat12 == null || mFormat24 == null) {
LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
if (mFormat12 == null) {
mFormat12 = ld.timeFormat12;
}
if (mFormat24 == null) {
mFormat24 = ld.timeFormat24;
}
}
//创建Calender对象mTimer
createTime(mTimeZone);
// Wait until onAttachedToWindow() to handle the ticker
chooseFormat(false);
}

 createTime()方法比较简单,我们直接看chooseFormat()方法,该方法会先确定时间日期格式,是12小时制还是24小时制,我们这里为了方便,将这确定的格式称为确定的格式,然后根据确定的格式判断要不要开始每秒更新时间显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
* depending on whether the user has selected 24-hour format.
*
* @param handleTicker true if calling this method should schedule/unschedule the
* time ticker, false otherwise
*/
private void chooseFormat(boolean handleTicker) {
//根据系统设置判断是否是24小时制
final boolean format24Requested = is24HourModeEnabled();
//LoacalData是支持ICU的类,主要是为了支持国际化
LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
if (format24Requested) {
//abc(CharSequence a, CharSequence b, CharSequence c),是判断a是否为null,不为null就返回a的值,
//如果a为null,则继续判断b是否为null,以此类推下去
mFormat = abc(mFormat24, mFormat12, ld.timeFormat24);
} else {
mFormat = abc(mFormat12, mFormat24, ld.timeFormat12);
}
//hadSeconds 记录之前的值
boolean hadSeconds = mHasSeconds;
//判断当前格式中是否包含秒
mHasSeconds = DateFormat.hasSeconds(mFormat);
//handleTicker参数、onAttachedToWindow()是否被调用,mHaseSeconds值是否有改变,
//当三个参数都为true时,才会根据mHaseSeconds之前的状态,来判断是否需要每秒更新时间
if (handleTicker && mAttached && hadSeconds != mHasSeconds) {
if (hadSeconds) getHandler().removeCallbacks(mTicker);
else mTicker.run();
}
}

我们可以看下mTicker变量的初始化,mTicker 是个一个实现了Runnale接口的匿名内部类对象。mTicker就是用于每秒更新时间的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final Runnable mTicker = new Runnable() {
public void run() {
//更新时间
onTimeChanged();
long now = SystemClock.uptimeMillis();
long next = now + (1000 - now % 1000);//下一个整数秒再次更新时间
getHandler().postAtTime(mTicker, next);//通过Handler自发自收消息
}
};
//更新时间显示
private void onTimeChanged() {
mTime.setTimeInMillis(System.currentTimeMillis());
setText(DateFormat.format(mFormat, mTime));
}

有哪些地方会去启动这一个更新时间的任务,也就是调用mTicker.run()呢。在上面提到的chooseFormat(boolean handleTicker)中会调用,另一处是在onAttachedToWindow()方法中也会调用。我们看下onAttachedToWindow()的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mAttached) {
mAttached = true;
registerReceiver(); //注册广播
registerObserver(); //注册ContentObserver,监听SettingProvider中时间日期格式等的改变
createTime(mTimeZone);
if (mHasSeconds) { //判断确定的格式中是否包含秒
mTicker.run();
} else {
onTimeChanged();
}
}
}

我们可以看到,在onAttachedToWindow()方法中,会根据确定的格式中是否包含秒,如果包含就开始每秒更新一次时间日期显示。或许你我提出个问题,上面我们一直提到,确定的格式中包含秒时,才会每秒去更新时间日期显示,如果不包含,那时间怎么更新呢?难道就不能更新了?我们看看registerReceiver()方法以及注册的广播。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void registerReceiver() {
final IntentFilter filter = new IntentFilter();
//ACTION_TIME_TICK是系统每分钟发出,通过这个广播实现了在没调用mTicker.run()方法时仍能更新时间
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
getContext().registerReceiver(mIntentReceiver, filter, null, getHandler());
}
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
final String timeZone = intent.getStringExtra("time-zone");
createTime(timeZone);
}
onTimeChanged();
}
};
/**
* 更新时间
*/
private void onTimeChanged() {
mTime.setTimeInMillis(System.currentTimeMillis());
setText(DateFormat.format(mFormat, mTime));
setContentDescription(DateFormat.format(mDescFormat, mTime));
}

 最后,我们通过一张流程图来总结下:

image

2,格式匹配方式

 在了解时间日期更新机制之后,我们看下当设置时间或者日期格式后,为什么就能够返回我们想要的格式?为什么在上一节,我们最后提到的设置格式YYYY'X'M'X'd 不能返回我们期望的日期格式。再看下onTimeChange()方法:

1
2
3
4
5
private void onTimeChanged() {
mTime.setTimeInMillis(System.currentTimeMillis());
setText(DateFormat.format(mFormat, mTime));
setContentDescription(DateFormat.format(mDescFormat, mTime));
}

更新时间显示是通过调用了setText(DateFormat.format(mFormat, mTime)); 方法,那么就可以继续看DateFormat.format()具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
* Given a format string and a {@link java.util.Calendar} object, returns a CharSequence
* containing the requested date.
* @param inFormat the format string, as described in {@link android.text.format.DateFormat}
* @param inDate the date to format
* @return a {@link CharSequence} containing the requested text
*/
public static CharSequence format(CharSequence inFormat, Calendar inDate) {
SpannableStringBuilder s = new SpannableStringBuilder(inFormat);//将inFormat转化可变的字符串
int count;
LocaleData localeData = LocaleData.get(Locale.getDefault());
int len = inFormat.length();
for (int i = 0; i < len; i += count) {
count = 1;
int c = s.charAt(i);
if (c == QUOTE) {
count = appendQuotedText(s, i, len);
len = s.length();
continue;
}
while ((i + count < len) && (s.charAt(i + count) == c)) {
count++;
}
//replacement就是要替换inFormat中的时间日期的内容;比如inFormat是:“mm:ss”,当前时间是12分23秒
//如果c为第一个“m”,那么replacement 就是“1”,
String replacement;
switch (c) {
case 'A': // 24小时制中的上午或者下午标志
case 'a':
replacement = localeData.amPm[inDate.get(Calendar.AM_PM) - Calendar.AM];
break;
case 'd': // 一个月中第几天
replacement = zeroPad(inDate.get(Calendar.DATE), count);
break;
case 'c': // 星期几
case 'E':
replacement = getDayOfWeekString(localeData,
inDate.get(Calendar.DAY_OF_WEEK), count, c);
break;
case 'K': // hour in am/pm (0-11)
case 'h': // hour in am/pm (1-12)
{
int hour = inDate.get(Calendar.HOUR);
if (c == 'h' && hour == 0) {
hour = 12;
}
replacement = zeroPad(hour, count);
}
break;
case 'H': // hour in day (0-23)
case 'k': // hour in day (1-24) [but see note below]
{
int hour = inDate.get(Calendar.HOUR_OF_DAY);
// Historically on Android 'k' was interpreted as 'H', which wasn't
// implemented, so pretty much all callers that want to format 24-hour
// times are abusing 'k'. http://b/8359981.
if (false && c == 'k' && hour == 0) {
hour = 24;
}
replacement = zeroPad(hour, count);
}
break;
case 'L': //一年中的月份
case 'M':
replacement = getMonthString(localeData,
inDate.get(Calendar.MONTH), count, c);
break;
case 'm': //一小时中的第几分钟
replacement = zeroPad(inDate.get(Calendar.MINUTE), count);
break;
case 's': //一分钟第几秒
replacement = zeroPad(inDate.get(Calendar.SECOND), count);
break;
case 'y': //年份
replacement = getYearString(inDate.get(Calendar.YEAR), count);
break;
case 'z': //时区
replacement = getTimeZoneString(inDate, count);
break;
default:
replacement = null;
break;
}
if (replacement != null) {
s.replace(i, i + count, replacement); //替换时间日期格式中对应的字符(即上面case 语句中的字符)
count = replacement.length(); // CARE: count is used in the for loop above
len = s.length();
}
}
if (inFormat instanceof Spanned) {
return new SpannedString(s);
} else {
return s.toString();
}
}

通过这个方法,我们会发现该方法里面定义的格式的字符和SimpleDateFormat 中列举的是有出入的,这也解释了在使用的章节里面,最后的一个问题:为什么YYYY'X'M'X'd 不能得到我们期望的日期。因为在DateFormat.format()方法中的case语句中根本没有格式”Y” 啊! 所以当我们在定义TextClock的format时最好的方式就是参考DateFormat.format()方法,看看它支持哪些格式