目录
一、Clock应用的作用和重要性
二、Clock应用的类型和功能
三、Clock应用的架构和组件
四、Clock应用的定制和扩展
Clock应用的作用和重要性如下表所示:
作用/重要性 | 描述 |
---|---|
1. 时间显示 | Clock应用用于显示当前时间,包括小时、分钟和秒数。它是用户在设备上快速查看时间的主要途径,因此在操作系统中具有重要作用。 |
2. 闹钟功能 | Clock应用通常提供闹钟功能,允许用户设置闹钟时间和重复周期。闹钟功能使用户能够设定特定的时间,以触发提醒或者唤醒操作。 |
3. 倒计时器 | Clock应用可能包含倒计时器功能,允许用户设置特定时间的倒计时,并在倒计时结束时触发提醒。倒计时器功能常用于计时器、定时器、比赛等场景。 |
4. 世界时钟 | Clock应用可以提供世界时钟功能,允许用户添加多个城市的时钟,以方便查看不同地区的时间。世界时钟功能对于跨时区的用户或者需要关注多个地区时间的用户非常重要。 |
5. 计时器 | Clock应用可能包含计时器功能,允许用户计时特定的时间间隔。计时器功能常用于测量时间、运动训练、烹饪等需要精确计时的场景。 |
时钟应用的作用和重要性,包括时间显示、闹钟功能、倒计时器、世界时钟和计时器。时钟应用在用户日常生活中起着重要的作用,帮助用户管理时间、安排日程和提醒重要事项。
Clock应用的类型和功能如下表所示:
类型/功能 | 描述 |
---|---|
1. 数字时钟 | 数字时钟是Clock应用最基本的功能之一,它以数字的形式显示当前时间。用户可以通过数字时钟快速了解当前的小时、分钟和秒数。 |
2. 模拟时钟 | 模拟时钟模拟了传统的机械时钟的外观和运行方式,包括指针和刻度盘。它可以提供更具艺术感和复古风格的时间显示方式。 |
3. 闹钟功能 | Clock应用通常包含闹钟功能,允许用户设置特定的时间和重复周期,以触发提醒或唤醒操作。用户可以设置多个闹钟,并根据自己的需求进行个性化配置。 |
4. 倒计时器 | 倒计时器允许用户设置特定的时间间隔,并在倒计时结束时触发提醒。倒计时器常用于计时器、定时器、比赛等场景,帮助用户进行时间管理和控制。 |
5. 世界时钟 | 世界时钟功能允许用户添加多个城市的时钟,以方便查看不同地区的时间。用户可以同时查看多个城市的时间,方便跨时区的用户或需要关注多个地区时间的用户。 |
6. 计时器 | 计时器功能允许用户计时特定的时间间隔。用户可以使用计时器进行测量时间、运动训练、烹饪等需要精确计时的活动,以实现精准的时间控制。 |
时钟应用的类型和功能,包括数字时钟、模拟时钟、闹钟功能、倒计时器、世界时钟和计时器。时钟应用可以根据用户的需求和喜好,提供不同类型的时钟显示和多种功能,帮助用户管理时间、提醒重要事项和满足个性化的需求。
时钟应用的架构和组件如下表所示:
架构/组件 | 描述 |
---|---|
1. 用户界面 (UI) | 时钟应用的用户界面包括时钟显示、设置界面和提醒界面等。用户界面负责展示当前时间、接收用户输入并与其他组件进行交互。 |
2. 时间管理 | 时间管理组件负责获取系统时间、处理时区信息、时间格式转换等功能。它提供了时钟所需的基本时间操作和计算功能。 |
3. 闹钟管理 | 闹钟管理组件负责管理用户设置的闹钟信息,包括闹钟的添加、删除、启用和禁用等操作。它还负责在设定的时间触发闹钟提醒或唤醒操作。 |
4. 计时器管理 | 计时器管理组件负责管理用户设置的计时器信息,包括计时器的开始、暂停、重置和结束等操作。它还负责在计时器结束时触发相应的提醒或操作。 |
5. 时钟存储 | 时钟存储组件用于存储用户设置的闹钟、计时器和其他相关数据。它可以将数据持久化保存,以便在应用关闭或设备重启后能够恢复数据。 |
6. 通知管理 | 通知管理组件负责发送时钟相关的通知,如闹钟提醒、计时器结束等。它将通知显示在系统通知栏或锁屏界面,提醒用户相关的时间事件。 |
时钟应用的架构和组件,包括用户界面、时间管理、闹钟管理、计时器管理、时钟存储和通知管理。这些组件共同协作,实现时钟应用的各项功能,包括时间显示、闹钟提醒、计时功能等。每个组件在时钟应用中扮演重要的角色,确保时钟应用的正常运行和提供良好的用户体验。
以系统应用的桌面时钟为例:
解决方案:
1.1 在AnalogClock.java中通过Canvas类来绘制表盘及刻度,如下:
//add begin
private Point mCoo = new Point(129, 219);
private Path mMainPath;
private Paint mMainPaint;
private void init() {
Log.d("test00","enter init");
mMainPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMainPaint.setStyle(Paint.Style.STROKE);
mMainPaint.setStrokeCap(Paint.Cap.ROUND);
mMainPath = new Path();
Log.d("test01","enter init");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float scale = getResources().getDisplayMetrics().density;
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
canvas.save();
canvas.translate(centerX, centerY);
canvas.scale(scale/2,scale/2);
drawBreakCircle(canvas);
drawDot(canvas);
drawText(canvas);
canvas.restore();
}
private void drawBreakCircle(Canvas canvas) {
mMainPaint.setStrokeWidth(2);
//mMainPaint.setColor(Color.parseColor("#D5D5D5"));//white
//mMainPaint.setColor(Color.parseColor("#000000"));//black
//mMainPaint.setColor(Color.parseColor("#ffffff"));//white
mMainPaint.setColor(Color.parseColor("#00000000"));//alpha=0
canvas.drawCircle(0f,0f,130f,mMainPaint);
}
/**
* draw pot
*
* @param canvas
*/
private void drawDot(Canvas canvas) {
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {
canvas.save();
canvas.rotate(30 * i);
mMainPaint.setStrokeWidth(5);
mMainPaint.setColor(Color.WHITE);
canvas.drawLine(155, 0, 170, 0, mMainPaint);
canvas.restore();
} else {
canvas.save();
canvas.rotate(6 * i);
mMainPaint.setStrokeWidth(4);
mMainPaint.setColor(Color.WHITE);
canvas.drawLine(160, 0, 170, 0, mMainPaint);
canvas.restore();
}
}
}
/**
* text
*
* @param canvas
*/
private void drawText(Canvas canvas) {
mMainPaint.setTextSize(25);
mMainPaint.setStrokeWidth(4);
mMainPaint.setStyle(Paint.Style.FILL);
mMainPaint.setTextAlign(Paint.Align.CENTER);
/*mMainPaint.setColor(Color.BLUE);*/
mMainPaint.setColor(Color.WHITE);
//test begin
canvas.drawText("3",143,7,mMainPaint);
canvas.drawText("6", 1, 145, mMainPaint);
canvas.drawText("9", -143,7, mMainPaint);
canvas.drawText("12", 0, -128, mMainPaint);
//others
canvas.drawText("1",72,-112,mMainPaint);
canvas.drawText("2", 121, -60, mMainPaint);
canvas.drawText("4", 122, 75, mMainPaint);
canvas.drawText("5", 70, 128, mMainPaint);
canvas.drawText("7", -70, 128, mMainPaint);
canvas.drawText("8", -122, 77, mMainPaint);
canvas.drawText("10", -118, -57, mMainPaint);
canvas.drawText("11",-67,-112,mMainPaint);
mMainPaint.setTextSize(25);
canvas.drawText("", 0, -150, mMainPaint);
}
//add end
public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
//add begin
Log.d("test03","analogClock");
init();
Log.d("teset04","analogClock");
mTime = Calendar.getInstance();
mDescFormat = ((SimpleDateFormat) DateFormat.getTimeFormat(context)).toLocalizedPattern();
// Must call mutate on these instances, otherwise the drawables will blur, because they're
// sharing their size characteristics with the (smaller) world cities analog clocks.
//del this
/*final ImageView dial = new AppCompatImageView(context);
dial.setImageResource(R.drawable.clock_analog_dial);
dial.getDrawable().mutate();
addView(dial);*/
mHourHand = new AppCompatImageView(context);
mHourHand.setImageResource(R.drawable.clock_analog_hour);
mHourHand.getDrawable().mutate();
addView(mHourHand);
mMinuteHand = new AppCompatImageView(context);
mMinuteHand.setImageResource(R.drawable.clock_analog_minute);
mMinuteHand.getDrawable().mutate();
addView(mMinuteHand);
mSecondHand = new AppCompatImageView(context);
mSecondHand.setImageResource(R.drawable.clock_analog_second);
mSecondHand.getDrawable().mutate();
addView(mSecondHand);
}
1.2 在ClockFragment.java中通过子线程来处理耗时操作:
//add begin
private View mAnalogClockView;
private final Timer timer = new Timer();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
super.onCreateView(inflater, container, icicle);
final View fragmentView = inflater.inflate(R.layout.clock_fragment, container, false);
mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
mCityAdapter = new SelectedCitiesAdapter(getActivity(), mDateFormat,
mDateFormatForAccessibility);
mCityList = (RecyclerView) fragmentView.findViewById(R.id.cities);
mCityList.setLayoutManager(new LinearLayoutManager(getActivity()));
mCityList.setAdapter(mCityAdapter);
mCityList.setItemAnimator(null);
DataModel.getDataModel().addCityListener(mCityAdapter);
final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
mCityList.addOnScrollListener(scrollPositionWatcher);
final Context context = container.getContext();
mCityList.setOnTouchListener(new CityListOnLongClickListener(context));
fragmentView.setOnLongClickListener(new StartScreenSaverListener());
// On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added
// on as a header to the main listview.
mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane);
if (mClockFrame != null) {
mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock);
mAnalogClock = (AnalogClock) mClockFrame.findViewById(R.id.analog_clock);
//add end
Log.d("test begin", String.valueOf("mAnalogClock == null"+ mAnalogClock == null));
mAnalogClock.postInvalidate();
//add by test begin
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d("ClockFragment01","test invalidate");
mAnalogClock.postInvalidate();
}
};
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
mHandler.sendEmptyMessage(0);//send message
}
};
timer.schedule(timerTask, 0, 0);
//add end
Utils.setClockIconTypeface(mClockFrame);
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame);
Utils.setClockStyle(mDigitalClock, mAnalogClock);
Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
}
// Schedule a runnable to update the date every quarter hour.
UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater, 100);
return fragmentView;
}
2.在desk_clock_saver.xml中调整屏保表盘的相对位置:
3. 新增一个ClockView.java用于绘图,并在world_clock_item.xml中引用该自定义View:
package com.android.deskclock;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.appcompat.widget.AppCompatImageView;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;
public class ClockView extends FrameLayout {
//add begin
private Point mCoo = new Point(75, 75);
private Path mMainPath;
private Paint mMainPaint;
public ClockView(Context context){
this(context,null);
}
public ClockView(Context context,AttributeSet attrs){
this(context,attrs,0);
}
public ClockView(Context context,AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
Log.d("test00","analogClock");
init();
Log.d("test01","analogClock");
mTime = Calendar.getInstance();
mDescFormat = ((SimpleDateFormat) DateFormat.getTimeFormat(context)).toLocalizedPattern();
mHourHand = new AppCompatImageView(context);
mHourHand.setImageResource(R.drawable.clock_analog_hour);
mHourHand.getDrawable().mutate();
addView(mHourHand);
mMinuteHand = new AppCompatImageView(context);
mMinuteHand.setImageResource(R.drawable.clock_analog_minute);
mMinuteHand.getDrawable().mutate();
addView(mMinuteHand);
}
private void init() {
Log.d("test02","enter init");
mMainPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMainPaint.setStyle(Paint.Style.STROKE);
mMainPaint.setStrokeCap(Paint.Cap.ROUND);
mMainPath = new Path();
Log.d("test03","enter init");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float scale = getResources().getDisplayMetrics().density;
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
canvas.save();
//canvas.translate(mCoo.x, mCoo.y);
canvas.translate(centerX, centerY);
canvas.scale(scale/2,scale/2);
drawBreakCircle(canvas);
drawDot(canvas);
drawText(canvas);
canvas.restore();
}
private void drawBreakCircle(Canvas canvas) {
mMainPaint.setStrokeWidth(2);
//mMainPaint.setColor(Color.parseColor("#D5D5D5"));//white
//mMainPaint.setColor(Color.parseColor("#000000"));//black
//mMainPaint.setColor(Color.parseColor("#ffffff"));//white
mMainPaint.setColor(Color.parseColor("#00000000"));//alpha=0
canvas.drawCircle(0f,0f,60f,mMainPaint);
}
/**
* draw pot
*
* @param canvas
*/
private void drawDot(Canvas canvas) {
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {
canvas.save();
canvas.rotate(30 * i);
mMainPaint.setStrokeWidth(3);
mMainPaint.setColor(Color.WHITE);
canvas.drawLine(74, 0, 84, 0, mMainPaint);
canvas.restore();
} else {
canvas.save();
canvas.rotate(6 * i);
mMainPaint.setStrokeWidth(2);
//mMainPaint.setColor(Color.BLUE);
mMainPaint.setColor(Color.WHITE);
canvas.drawLine(79, 0, 84, 0, mMainPaint);
canvas.restore();
}
}
}
/**
* text
*
* @param canvas
*/
private void drawText(Canvas canvas) {
mMainPaint.setTextSize(15);
mMainPaint.setStrokeWidth(3);
mMainPaint.setStyle(Paint.Style.FILL);
mMainPaint.setTextAlign(Paint.Align.CENTER);
/*mMainPaint.setColor(Color.BLUE);*/
mMainPaint.setColor(Color.WHITE);
//test begin
canvas.drawText("3",67,4,mMainPaint);
canvas.drawText("6", 0, 69, mMainPaint);
canvas.drawText("9", -67, 5, mMainPaint);
canvas.drawText("12", 0, -59, mMainPaint);
//others
canvas.drawText("1",33,-51,mMainPaint);
canvas.drawText("2", 57, -27, mMainPaint);
canvas.drawText("4", 57, 36, mMainPaint);
canvas.drawText("5", 33, 60, mMainPaint);
canvas.drawText("7", -33, 61, mMainPaint);
canvas.drawText("8", -57, 37, mMainPaint);
canvas.drawText("10", -54, -27, mMainPaint);
canvas.drawText("11",-31,-51,mMainPaint);
mMainPaint.setTextSize(15);
canvas.drawText("", 0, -150, mMainPaint);
}
//add end
private final ImageView mHourHand;
private final ImageView mMinuteHand;
private Calendar mTime;
private String mDescFormat;
private TimeZone mTimeZone;
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 tz = intent.getStringExtra(Intent.EXTRA_TIMEZONE);
mTime = Calendar.getInstance(TimeZone.getTimeZone(tz));
}
onTimeChanged();
}
};
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
getContext().registerReceiver(mIntentReceiver, filter);
// Refresh the calendar instance since the time zone may have changed while the receiver
// wasn't registered.
mTime = Calendar.getInstance(mTimeZone != null ? mTimeZone : TimeZone.getDefault());
onTimeChanged();
}
private void onTimeChanged() {
mTime.setTimeInMillis(System.currentTimeMillis());
final float hourAngle = mTime.get(Calendar.HOUR) * 30f;
mHourHand.setRotation(hourAngle);
final float minuteAngle = mTime.get(Calendar.MINUTE) * 6f;
mMinuteHand.setRotation(minuteAngle);
setContentDescription(DateFormat.format(mDescFormat, mTime));
invalidate();
}
public void setTimeZone(String id) {
mTimeZone = TimeZone.getTimeZone(id);
mTime.setTimeZone(mTimeZone);
onTimeChanged();
}
}
在world_clock_item.xml中引用该自定义View:
4.在AlarmActivity.java中的dispatchKeyEvent()方法中添加处理Enter键的逻辑:
private boolean mPressedEnterKey = false;
@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) {
// Do this in dispatch to intercept a few of the system keys.
LOGGER.v("dispatchKeyEvent: %s", keyEvent);
final int keyCode = keyEvent.getKeyCode();
switch (keyCode) {
//>> add begin
case KeyEvent.KEYCODE_ENTER:
mPressedEnterKey = (keyEvent.getAction() == KeyEvent.ACTION_UP) ? true:false;
break;
//<<
// Volume keys and camera keys dismiss the alarm.
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE:
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_CAMERA:
case KeyEvent.KEYCODE_FOCUS:
if (!mAlarmHandled) {
switch (mVolumeBehavior) {
case SNOOZE:
if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
snooze();
}
return true;
case DISMISS:
if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
dismiss();
}
return true;
}
}
}
return super.dispatchKeyEvent(keyEvent);
}
@Override
public void onClick(View view) {
if (mAlarmHandled) {
LOGGER.v("onClick ignored: %s", view);
return;
}
LOGGER.v("onClick: %s", view);
// If in accessibility mode, allow snooze/dismiss by double tapping on respective icons.
if (isAccessibilityEnabled()) {
if (view == mSnoozeButton) {
snooze();
} else if (view == mDismissButton) {
dismiss();
}
return;
}
//>> add begin
if (mPressedEnterKey) {
if (view == mSnoozeButton) {
snooze();
} else if (view == mDismissButton) {
dismiss();
}
return;
}
//<<
if (view == mSnoozeButton) {
hintSnooze();
} else if (view == mDismissButton) {
hintDismiss();
}
}
@Override
public boolean onTouch(View view, MotionEvent event) {
if (mAlarmHandled) {
LOGGER.v("onTouch ignored: %s", event);
return false;
}
//>> add begin
mPressedEnterKey = false;
//<<
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
LOGGER.v("onTouch started: %s", event);
// Track the pointer that initiated the touch sequence.
mInitialPointerIndex = event.getPointerId(event.getActionIndex());
// Stop the pulse, allowing the last pulse to finish.
mPulseAnimator.setRepeatCount(0);
} else if (action == MotionEvent.ACTION_CANCEL) {
LOGGER.v("onTouch canceled: %s", event);
// Clear the pointer index.
mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
// Reset everything.
resetAnimations();
}
final int actionIndex = event.getActionIndex();
if (mInitialPointerIndex == MotionEvent.INVALID_POINTER_ID
|| mInitialPointerIndex != event.getPointerId(actionIndex)) {
// Ignore any pointers other than the initial one, bail early.
return true;
}
final int[] contentLocation = {0, 0};
mContentView.getLocationOnScreen(contentLocation);
final float x = event.getRawX() - contentLocation[0];
final float y = event.getRawY() - contentLocation[1];
final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
final float snoozeFraction, dismissFraction;
if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
snoozeFraction = getFraction(alarmRight, mSnoozeButton.getLeft(), x);
dismissFraction = getFraction(alarmLeft, mDismissButton.getRight(), x);
} else {
snoozeFraction = getFraction(alarmLeft, mSnoozeButton.getRight(), x);
dismissFraction = getFraction(alarmRight, mDismissButton.getLeft(), x);
}
setAnimatedFractions(snoozeFraction, dismissFraction);
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
LOGGER.v("onTouch ended: %s", event);
mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
if (snoozeFraction == 1.0f) {
snooze();
} else if (dismissFraction == 1.0f) {
dismiss();
} else {
if (snoozeFraction > 0.0f || dismissFraction > 0.0f) {
// Animate back to the initial state.
AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator);
} else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) {
// User touched the alarm button, hint the dismiss action.
hintDismiss();
}
// Restart the pulse.
mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
if (!mPulseAnimator.isStarted()) {
mPulseAnimator.start();
}
}
}
return true;
}
5.在desk_clock.xml中为Toolbar设置touchscreenBlocksFocus属性和facusable属性:
6.在CitySelectionActivity.java中添加一个判空操作:
mCitiesList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
final CheckBox b = view.findViewById(R.id.city_onoff);
//add begin
if(null != b){
b.setChecked(!b.isChecked());
}
//add end
}
});
本文仅代表个人观点和经验,难免存在不足之处。如果有任何错误或改进的建议,欢迎指正和交流,共同进步。