Huangyuyue #3

Merged
pqmra35s2 merged 3 commits from Huangyuyue into main 1 year ago

@ -0,0 +1,213 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明表明该类所在的包名此处在net.micode.notes.ui包下
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.Window;
import android.view.WindowManager;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import java.io.IOException;
// AlarmAlertActivity类继承自Activity类实现了OnClickListener和OnDismissListener接口用于处理闹钟提醒相关的界面展示、声音播放等功能
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
// 记录笔记的ID用于后续操作中标识具体的笔记
private long mNoteId;
// 笔记的摘要内容,用于在提醒对话框中展示部分笔记内容
private String mSnippet;
// 定义摘要预览的最大长度为60个字符
private static final int SNIPPET_PREW_MAX_LEN = 60;
// 用于播放闹钟提醒声音的MediaPlayer对象
MediaPlayer mPlayer;
// 重写Activity的onCreate方法在Activity创建时被调用进行一些初始化操作
@Override
protected void onCreate(Bundle savedInstanceState) {
// 调用父类的onCreate方法完成Activity创建的基本初始化流程这是必须的操作
super.onCreate(savedInstanceState);
// 请求去除Activity的标题栏使界面更加简洁通常用于全屏等特定显示需求
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 获取当前Activity的窗口对象后续用于设置窗口相关的属性
final Window win = getWindow();
// 给窗口添加标志位使得在屏幕锁定时也能显示该Activity方便用户看到闹钟提醒
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// 判断屏幕是否处于开启状态
if (!isScreenOn()) {
// 如果屏幕未开启,添加一系列窗口标志位,用于控制屏幕相关的行为,比如保持屏幕常亮、点亮屏幕等,确保闹钟提醒能正常展示给用户
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
}
// 获取启动该Activity的Intent对象用于获取传递过来的数据等信息
Intent intent = getIntent();
try {
// 从Intent携带的数据中获取笔记的ID具体是从数据的路径片段中的第二个元素获取假设数据格式符合特定规范
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
// 通过工具类DataUtils根据笔记ID获取笔记的摘要内容
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 如果获取到的摘要内容长度大于预设的最大长度,进行截断处理,并添加提示信息,以合适的长度展示摘要
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} catch (IllegalArgumentException e) {
// 如果在获取笔记ID或处理摘要过程中出现参数异常打印异常堆栈信息并且直接返回不再继续执行后续可能出错的操作
e.printStackTrace();
return;
}
// 创建一个MediaPlayer对象用于播放闹钟提醒的声音
mPlayer = new MediaPlayer();
// 判断该笔记是否在笔记数据库中可见通过DataUtils工具类的方法进行判断
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
// 如果可见,展示操作对话框,给用户提供相关操作选项
showActionDialog();
// 调用方法播放闹钟提醒声音
playAlarmSound();
} else {
// 如果笔记不可见直接结束该Activity不需要进行后续的提醒相关操作
finish();
}
}
// 方法用于判断屏幕是否处于开启状态通过获取PowerManager服务并调用其isScreenOn方法来判断
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
// 方法用于播放闹钟提醒声音,进行一系列设置并启动声音播放
private void playAlarmSound() {
// 获取系统默认的闹钟铃声的Uri通过RingtoneManager来获取实际使用的默认铃声的Uri
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
// 获取系统设置中受静音模式影响的音频流相关设置,用于后续判断闹钟音频流的设置情况
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
// 判断静音模式下是否影响闹钟音频流如果受影响则按照相应设置设置MediaPlayer的音频流类型
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
// 如果不受影响将MediaPlayer的音频流类型设置为普通的闹钟音频流类型
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
try {
// 设置MediaPlayer的数据源为获取到的闹钟铃声的Uri指定从哪里获取音频数据来播放
mPlayer.setDataSource(this, url);
// 准备MediaPlayer使其进入可播放状态加载音频数据等资源
mPlayer.prepare();
// 设置MediaPlayer循环播放使得闹钟声音一直响直到被用户操作停止
mPlayer.setLooping(true);
// 启动MediaPlayer开始播放闹钟声音
mPlayer.start();
} catch (IllegalArgumentException e) {
// 如果在设置数据源等操作中出现参数异常,打印异常堆栈信息
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// 如果出现安全相关异常,比如没有权限访问音频资源等情况,打印异常堆栈信息
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// 如果MediaPlayer处于不正确的状态比如已经在播放等情况导致操作失败打印异常堆栈信息
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// 如果在读取音频数据等操作中出现IO异常比如文件不存在等情况打印异常堆栈信息
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 方法用于展示操作对话框,给用户提供针对闹钟提醒的操作选项,比如确认、进入编辑等
private void showActionDialog() {
// 创建一个AlertDialog的构建器用于构建自定义的对话框
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
// 设置对话框的标题为应用的名称,通过资源文件中的字符串资源来设置
dialog.setTitle(R.string.app_name);
// 设置对话框的消息内容为前面获取和处理后的笔记摘要内容,展示给用户提醒相关的笔记信息
dialog.setMessage(mSnippet);
// 设置对话框的确认按钮通常是正向操作按钮的文本及点击监听器点击时会触发OnClickListener中的相应逻辑
dialog.setPositiveButton(R.string.notealert_ok, this);
// 判断屏幕是否开启,如果开启,设置对话框的取消按钮(通常是进入相关操作按钮)的文本及点击监听器
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
}
// 构建并显示对话框,同时给对话框设置关闭监听器,当对话框关闭时会触发相应逻辑
dialog.show().setOnDismissListener(this);
}
// 实现OnClickListener接口的方法处理对话框按钮点击事件根据点击的按钮执行相应的操作
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
// 如果点击的是取消按钮通常是进入相关操作按钮创建一个Intent用于启动笔记编辑Activity
Intent intent = new Intent(this, NoteEditActivity.class);
// 设置Intent的动作这里设置为查看动作表明是查看笔记详情的意图
intent.setAction(Intent.ACTION_VIEW);
// 将笔记的ID作为额外数据添加到Intent中传递给NoteEditActivity以便其能识别要编辑的具体笔记
intent.putExtra(Intent.EXTRA_UID, mNoteId);
// 启动NoteEditActivity切换到笔记编辑界面
startActivity(intent);
break;
default:
break;
}
}
// 实现OnDismissListener接口的方法当对话框关闭时被调用用于停止闹钟声音并结束当前Activity
public void onDismiss(DialogInterface dialog) {
// 调用方法停止闹钟声音,释放相关资源
stopAlarmSound();
// 结束当前Activity关闭闹钟提醒界面
finish();
}
// 方法用于停止闹钟声音释放MediaPlayer相关的资源将其设置为null
private void stopAlarmSound() {
if (mPlayer!= null) {
// 停止MediaPlayer的播放
mPlayer.stop();
// 释放MediaPlayer占用的资源如音频缓冲区等
mPlayer.release();
// 将MediaPlayer对象设置为null避免后续误操作
mPlayer = null;
}
}
}

@ -0,0 +1,88 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明表明该类所在的包名为net.micode.notes.ui
package net.micode.notes.ui;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
// AlarmInitReceiver类继承自BroadcastReceiver用于接收广播并处理与闹钟初始化相关的逻辑比如根据条件设置闹钟提醒等操作
public class AlarmInitReceiver extends BroadcastReceiver {
// 定义一个字符串数组用于指定从数据库查询时需要获取的列名这里获取笔记的ID和提醒日期这两列信息
private static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE
};
// 定义常量用于表示查询结果中笔记ID所在列的索引方便后续从Cursor中获取对应数据值为0表示第一列
private static final int COLUMN_ID = 0;
// 定义常量用于表示查询结果中提醒日期所在列的索引方便后续从Cursor中获取对应数据值为1表示第二列
private static final int COLUMN_ALERTED_DATE = 1;
// 重写BroadcastReceiver的onReceive方法当接收到广播时会被调用在此方法中实现具体的业务逻辑
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前的系统时间(以毫秒为单位),用于后续与笔记的提醒日期进行比较,判断哪些笔记需要设置闹钟提醒
long currentDate = System.currentTimeMillis();
// 通过ContentResolver发起一个数据库查询操作从指定的笔记内容URINotes.CONTENT_NOTE_URI中查询数据
// 查询的列由PROJECTION指定查询条件是提醒日期大于当前日期并且笔记类型为普通笔记Notes.TYPE_NOTE
// 条件中的参数通过后面的字符串数组传入当前时间的值
// 最后一个参数null表示查询不需要排序等额外设置
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) },
null);
// 判断查询结果的Cursor是否为空如果不为空表示有满足条件的数据进行后续操作
if (c!= null) {
// 将Cursor的指针移动到第一条数据位置如果有数据则返回true可以开始遍历数据
if (c.moveToFirst()) {
// 使用do-while循环来遍历查询结果集确保至少会执行一次循环体中的操作
do {
// 从Cursor中获取提醒日期这一列的数据通过之前定义的列索引COLUMN_ALERTED_DATE得到具体的提醒时间戳
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 创建一个Intent用于指定当闹钟触发时要启动的组件这里指定为AlarmReceiver类
Intent sender = new Intent(context, AlarmReceiver.class);
// 给Intent设置数据将笔记的ID附加到对应的内容URI上这样可以传递具体是哪个笔记的闹钟触发了
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 创建一个PendingIntent用于包装前面创建的Intent以便交给AlarmManager使用
// 这里的请求码设置为0表示没有特定的请求标识并且使用的是广播类型的PendingIntent
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
// 获取系统的AlarmManager服务用于设置闹钟相关的操作
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 通过AlarmManager设置一个闹钟使用RTC_WAKEUP模式表示在指定的绝对时间唤醒设备来触发闹钟
// 指定提醒时间为前面获取的alertDate并且关联对应的PendingIntent当时间到达时会触发对应的广播
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext()); // 判断是否还有下一条数据,如果有则继续循环执行操作,处理下一条满足条件的笔记数据
}
// 关闭Cursor释放相关的资源避免内存泄漏等问题
c.close();
}
}
}

@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明表明该类所在的包名为net.micode.notes.ui
package net.micode.notes.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
// AlarmReceiver类继承自BroadcastReceiver用于接收广播消息并在接收到广播后进行相应的操作这里主要是启动相关的Activity
public class AlarmReceiver extends BroadcastReceiver {
// 重写BroadcastReceiver的onReceive方法当该广播接收器接收到广播时此方法会被调用用于处理具体的业务逻辑
@Override
public void onReceive(Context context, Intent intent) {
// 将传入的Intent的目标组件设置为AlarmAlertActivity.class也就是指定当这个广播触发后要启动的Activity是AlarmAlertActivity
intent.setClass(context, AlarmAlertActivity.class);
// 给Intent添加一个标志位FLAG_ACTIVITY_NEW_TASK表示以新任务的形式启动Activity。
// 这通常是在从广播接收器启动Activity时需要添加的因为广播接收器没有自己的任务栈需要明确告知系统以新任务来启动Activity
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 通过传入的Context对象启动设置好的Activity使得应用能够从接收到广播的这个逻辑跳转到对应的AlarmAlertActivity展示闹钟提醒相关的界面等内容
context.startActivity(intent);
}
}

@ -0,0 +1,485 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import net.micode.notes.R;
import android.content.Context;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
public class DateTimePicker extends FrameLayout {
private static final boolean DEFAULT_ENABLE_STATE = true;
private static final int HOURS_IN_HALF_DAY = 12;
private static final int HOURS_IN_ALL_DAY = 24;
private static final int DAYS_IN_ALL_WEEK = 7;
private static final int DATE_SPINNER_MIN_VAL = 0;
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
private static final int MINUT_SPINNER_MIN_VAL = 0;
private static final int MINUT_SPINNER_MAX_VAL = 59;
private static final int AMPM_SPINNER_MIN_VAL = 0;
private static final int AMPM_SPINNER_MAX_VAL = 1;
private final NumberPicker mDateSpinner;
private final NumberPicker mHourSpinner;
private final NumberPicker mMinuteSpinner;
private final NumberPicker mAmPmSpinner;
private Calendar mDate;
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
private boolean mIsAm;
private boolean mIs24HourView;
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
private boolean mInitialising;
private OnDateTimeChangedListener mOnDateTimeChangedListener;
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
updateDateControl();
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
if (!mIs24HourView) {
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
updateAmPmControl();
}
} else {
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
onDateTimeChanged();
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0;
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
} else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mIsAm = !mIsAm;
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
updateAmPmControl();
onDateTimeChanged();
}
};
public interface OnDateTimeChangedListener {
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
mDate = Calendar.getInstance();
mInitialising = true;
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
inflate(context, R.layout.datetime_picker, this);
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// update controls to initial state
updateDateControl();
updateHourControl();
updateAmPmControl();
set24HourView(is24HourView);
// set to current time
setCurrentDate(date);
setEnabled(isEnabled());
// set the content descriptions
mInitialising = false;
}
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
mIsEnabled = enabled;
}
@Override
public boolean isEnabled() {
return mIsEnabled;
}
/**
* Get the current date in millis
*
* @return the current date in millis
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
* Set the current date
*
* @param date The current date in millis
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
}
/**
* Set the current date
*
* @param year The current year
* @param month The current month
* @param dayOfMonth The current dayOfMonth
* @param hourOfDay The current hourOfDay
* @param minute The current minute
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
setCurrentHour(hourOfDay);
setCurrentMinute(minute);
}
/**
* Get current year
*
* @return The current year
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
* Set current year
*
* @param year The current year
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
return;
}
mDate.set(Calendar.YEAR, year);
updateDateControl();
onDateTimeChanged();
}
/**
* Get current month in the year
*
* @return The current month in the year
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
* Set current month in the year
*
* @param month The month in the year
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
return;
}
mDate.set(Calendar.MONTH, month);
updateDateControl();
onDateTimeChanged();
}
/**
* Get current day of the month
*
* @return The day of the month
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
* Set current day of the month
*
* @param dayOfMonth The day of the month
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return;
}
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
updateDateControl();
onDateTimeChanged();
}
/**
* Get current hour in 24 hour mode, in the range (0~23)
* @return The current hour in 24 hour mode
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY;
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
}
}
}
/**
* Set current hour in 24 hour mode, in the range (0~23)
*
* @param hourOfDay
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false;
if (hourOfDay > HOURS_IN_HALF_DAY) {
hourOfDay -= HOURS_IN_HALF_DAY;
}
} else {
mIsAm = true;
if (hourOfDay == 0) {
hourOfDay = HOURS_IN_HALF_DAY;
}
}
updateAmPmControl();
}
mHourSpinner.setValue(hourOfDay);
onDateTimeChanged();
}
/**
* Get currentMinute
*
* @return The Current Minute
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
* Set current minute
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
return;
}
mMinuteSpinner.setValue(minute);
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged();
}
/**
* @return true if this is in 24 hour view else false.
*/
public boolean is24HourView () {
return mIs24HourView;
}
/**
* Set whether in 24 hour or AM/PM mode.
*
* @param is24HourView True for 24 hour mode. False for AM/PM mode.
*/
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) {
return;
}
mIs24HourView = is24HourView;
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
updateHourControl();
setCurrentHour(hour);
updateAmPmControl();
}
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null);
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
mDateSpinner.setDisplayedValues(mDateDisplayValues);
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
mDateSpinner.invalidate();
}
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
} else {
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
/**
* Set the callback that indicates the 'Set' button has been pressed.
* @param callback the callback, if null will do nothing
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}
}

@ -0,0 +1,143 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
* Apache License 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 声明该类所属的包名表明这个类位于net.micode.notes.ui包下在项目的包结构中有其特定的组织和访问层级关系。
package net.micode.notes.ui;
// 导入必要的类库这里导入了Calendar类用于处理日期和时间相关的操作比如获取、设置具体的年、月、日、时、分等信息。
import java.util.Calendar;
// 导入R类这个类通常是由Android的资源编译系统自动生成的用于访问项目中的各种资源如布局文件、字符串资源、图片资源等这里主要用于获取相关的字符串资源等。
import net.micode.notes.R;
// 导入DateTimePicker类应该是自定义的用于选择日期和时间的控件类提供了日期时间选择的相关功能和界面交互逻辑。
import net.micode.notes.ui.DateTimePicker;
// 导入DateTimePicker类中定义的OnDateTimeChangedListener接口用于监听日期时间选择器中的日期时间发生变化的事件以便进行相应的处理操作。
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener;
// 导入相关的Android系统类AlertDialog用于创建弹出式的对话框提供一种提示、确认等交互的UI形式
// Context用于获取应用的上下文环境信息是很多Android操作的基础
// DialogInterface用于处理对话框相关的交互逻辑比如按钮点击等事件
// OnClickListener用于监听点击事件在这里用于监听对话框按钮的点击操作并执行相应逻辑。
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
// 导入用于处理日期时间格式化相关操作的类和方法比如判断是否是24小时制格式、按照指定格式格式化日期时间等功能。
import android.text.format.DateFormat;
import android.text.format.DateUtils;
// DateTimePickerDialog类继承自AlertDialog类意味着它是一个特殊的对话框用于展示日期时间选择的界面并实现了OnClickListener接口
// 用于处理对话框中按钮的点击事件,整体实现了一个可以让用户选择日期时间并在确定后进行相应回调处理的对话框功能。
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 定义一个Calendar类型的成员变量mDate用于存储当前日期时间选择器所表示的日期时间信息通过Calendar.getInstance()获取初始值,即当前系统时间对应的日期时间对象。
private Calendar mDate = Calendar.getInstance();
// 定义一个布尔类型的成员变量mIs24HourView用于标记当前日期时间选择器是否采用24小时制视图模式初始值会根据系统设置等情况后续进行设置。
private boolean mIs24HourView;
// 定义一个OnDateTimeSetListener类型的成员变量mOnDateTimeSetListener这是一个自定义的接口类型变量
// 外部类可以实现这个接口并传入该对话框,用于在用户点击对话框中的确定按钮并设置好日期时间后,接收相应的回调通知并执行自定义的业务逻辑。
private OnDateTimeSetListener mOnDateTimeSetListener;
// 定义一个DateTimePicker类型的成员变量mDateTimePicker这是前面导入的自定义日期时间选择器控件的实例用于在对话框中展示给用户进行日期时间的选择操作。
private DateTimePicker mDateTimePicker;
// 定义一个内部接口OnDateTimeSetListener外部类需要实现这个接口来监听用户在对话框中完成日期时间选择并点击确定按钮后的事件
// 接口中定义了OnDateTimeSet方法该方法会在相应事件触发时被调用传入当前的对话框实例以及用户选择的日期时间以毫秒为单位的时间戳表示作为参数方便外部类进行后续处理。
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date);
}
// 这是DateTimePickerDialog类的构造函数用于创建该对话框的实例接收一个Context类型的参数context用于获取应用的上下文环境信息
// 以及一个long类型的参数date用于指定初始的日期时间以毫秒为单位的时间戳形式表示对话框打开时日期时间选择器默认显示的日期时间。
public DateTimePickerDialog(Context context, long date) {
// 调用父类AlertDialog的构造函数传入上下文环境context完成父类的初始化操作确保这个自定义对话框能继承AlertDialog的相关属性和功能正常展示为一个对话框的形式。
super(context);
// 创建一个DateTimePicker的实例传入上下文环境context用于生成日期时间选择器控件这个控件将被添加到对话框中供用户进行日期时间的选择操作。
mDateTimePicker = new DateTimePicker(context);
// 将创建好的日期时间选择器控件mDateTimePicker设置为对话框的视图内容这样当对话框显示时用户就能看到并操作这个日期时间选择器了。
setView(mDateTimePicker);
// 为日期时间选择器mDateTimePicker设置日期时间变化监听器当用户在日期时间选择器上选择不同的日期、时间等操作导致日期时间发生变化时
// 会触发监听器中的onDateTimeChanged方法在这个方法内部会将选择器上的新日期时间信息更新到mDate对象中即存储对话框当前表示的日期时间的对象
// 并调用updateTitle方法根据新的日期时间更新对话框的标题显示内容使其始终能准确反映当前选择的日期时间情况。
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
updateTitle(mDate.getTimeInMillis());
}
});
// 将传入的初始日期时间以毫秒为单位的时间戳date设置到mDate对象中通过setTimeInMillis方法以此来初始化对话框中日期时间选择器默认显示的日期时间信息。
mDate.setTimeInMillis(date);
// 将mDate对象中表示秒的部分设置为0确保日期时间的精确性以及符合一般的日期时间使用场景通常在选择日期时间时不需要关注秒的设置这里统一初始化为0秒。
mDate.set(Calendar.SECOND, 0);
// 调用日期时间选择器mDateTimePicker的setCurrentDate方法传入mDate对象的时间戳通过getTimeInMillis方法获取
// 用于将日期时间选择器的显示状态更新为与mDate对象所表示的日期时间一致保证界面显示和内部存储的初始日期时间是匹配的。
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 设置对话框的确定按钮通过调用setButton方法传入从资源文件中获取的表示确定按钮文本的字符串通过context.getString方法获取R.string.datetime_dialog_ok对应的字符串资源
// 以及当前类this作为点击事件的监听器因为当前类实现了OnClickListener接口会处理确定按钮的点击事件使得用户点击确定按钮时能触发相应的逻辑。
setButton(context.getString(R.string.datetime_dialog_ok), this);
// 设置对话框的取消按钮通过调用setButton2方法传入从资源文件中获取的表示取消按钮文本的字符串通过context.getString方法获取R.string.datetime_dialog_cancel对应的字符串资源
// 以及传入null作为点击事件的监听器表示取消按钮点击后不做额外的处理一般就是关闭对话框等默认操作
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 调用set24HourView方法传入通过DateFormat.is24HourFormat方法获取的布尔值该方法用于判断当前系统是否采用24小时制格式
// 将获取到的布尔值传入set24HourView方法用于设置日期时间选择器是否为24小时制视图模式以此来保证日期时间选择器的显示模式与系统设置相符。
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 调用updateTitle方法传入mDate对象的时间戳通过getTimeInMillis方法获取用于初始化对话框的标题显示内容使其能根据初始的日期时间正确展示相关信息给用户。
updateTitle(mDate.getTimeInMillis());
}
// 用于设置日期时间选择器是否为24小时制视图模式的方法接收一个布尔类型的参数is24HourView将其值赋给成员变量mIs24HourView
// 以此来更新日期时间选择器的显示模式后续可以根据这个变量的值来调整日期时间选择器中相关控件如小时选择器、AM/PM选择器等的显示和操作逻辑。
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
// 用于设置日期时间设置监听器的方法接收一个OnDateTimeSetListener类型的参数callBack将其赋值给成员变量mOnDateTimeSetListener
// 外部类可以通过实现OnDateTimeSetListener接口并传入相应的实现类实例来监听用户在对话框中完成日期时间选择并点击确定按钮后的事件
// 以便在该事件触发时执行自定义的业务逻辑比如保存选择的日期时间到数据库、更新相关UI显示等操作。
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
// 私有方法用于更新对话框的标题显示内容根据传入的日期时间以毫秒为单位的时间戳date来进行格式化并设置为对话框的标题。
// 首先会定义一个整数类型的变量flag用于设置日期时间格式化的标志位通过按位或操作组合多个标志位确定要显示的日期时间信息的格式。
// 例如这里设置了显示年、显示日期、显示时间等标志位并且根据当前是否是24小时制视图模式由mIs24HourView变量决定来添加对应的24小时制或12小时制的格式化标志位。
// 最后通过DateUtils.formatDateTime方法传入应用的上下文环境this.getContext()、日期时间时间戳date以及格式化标志位flag
// 将格式化后的字符串设置为对话框的标题,使得对话框标题能准确展示当前选择的日期时间信息,并且符合相应的时间格式要求。
private void updateTitle(long date) {
int flag =
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR;
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
// 实现OnClickListener接口中的onClick方法用于处理对话框中按钮的点击事件这里主要处理确定按钮的点击事件因为取消按钮点击时没有额外设置监听器会执行默认操作
// 当用户点击确定按钮时会判断是否已经设置了日期时间设置监听器通过判断mOnDateTimeSetListener是否为null
// 如果已经设置了有效的监听器就会触发该监听器的OnDateTimeSet方法并传入当前的对话框实例this以及mDate对象的时间戳通过getTimeInMillis方法获取
// 使得外部实现了OnDateTimeSetListener接口的类能够接收到用户选择的日期时间信息并执行相应的业务逻辑比如保存选择的日期时间到数据库、更新相关UI显示等操作。
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener!= null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

@ -0,0 +1,98 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
* Apache License 2.0
*/
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 声明该类所属的包名表明这个类位于net.micode.notes.ui包下在项目的整体包结构中有其特定的组织和访问层级关系。
package net.micode.notes.ui;
// 导入必要的Android系统相关类Context用于获取应用的上下文环境信息是很多Android操作如资源获取、系统服务调用等的基础。
import android.content.Context;
// 导入用于操作菜单相关的类Menu用于表示菜单对象包含了菜单项等相关信息MenuItem用于表示具体的菜单项可以设置其属性、响应点击事件等。
import android.view.Menu;
import android.view.MenuItem;
// 导入View相关类View是Android中所有可视化组件的基类这里用于处理视图相关的操作和事件监听等
// OnClickListener用于监听视图的点击事件以便在视图被点击时执行相应的逻辑。
import android.view.View;
import android.view.View.OnClickListener;
// 导入Button类用于创建按钮控件按钮是常见的用户交互组件可响应用户点击操作等。
import android.widget.Button;
// 导入PopupMenu相关类PopupMenu用于创建弹出式菜单提供一种在特定视图通常是某个按钮等上点击弹出菜单选项的交互方式
// OnMenuItemClickListener用于监听弹出式菜单中菜单项的点击事件以便执行相应的业务逻辑。
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;
// 导入项目中自动生成的资源类R通过它可以访问项目中的各种资源如布局文件、图片资源、字符串资源等这里主要用于获取特定的图片资源。
import net.micode.notes.R;
// DropdownMenu类用于创建一个带有弹出式菜单功能的组件它关联一个按钮Button点击该按钮会弹出一个菜单PopupMenu
// 并且可以对菜单的相关属性如菜单项点击监听、查找菜单项、设置按钮显示文本等进行操作方便在Android应用中实现下拉菜单式的交互功能。
public class DropdownMenu {
// 定义一个Button类型的成员变量mButton用于存储关联的按钮控件这个按钮将作为触发弹出式菜单显示的入口。
private Button mButton;
// 定义一个PopupMenu类型的成员变量mPopupMenu用于存储创建的弹出式菜单对象通过这个对象可以进行弹出菜单的显示、设置菜单项点击监听等操作。
private PopupMenu mPopupMenu;
// 定义一个Menu类型的成员变量mMenu用于存储弹出式菜单对应的菜单对象通过它可以访问和操作菜单中的具体菜单项如添加、查找、移除菜单项等
private Menu mMenu;
// DropdownMenu类的构造函数用于创建该类的实例接收三个参数
// Context类型的context用于获取应用的上下文环境信息以便能够正确加载资源、创建相关的系统组件等
// Button类型的button是要关联的按钮控件点击这个按钮将弹出菜单
// int类型的menuId是菜单资源的ID用于指定要加载并显示的菜单布局资源通常在XML文件中定义了具体的菜单项等内容
public DropdownMenu(Context context, Button button, int menuId) {
// 将传入的按钮控件参数button赋值给成员变量mButton建立与外部传入按钮的关联后续通过这个成员变量对按钮进行相关操作如设置背景、添加点击监听等
mButton = button;
// 通过调用setBackgroundResource方法为按钮mButton设置背景图片资源这里使用了R.drawable.dropdown_icon所对应的图片资源
// 该图片通常会作为按钮的可视化标识,提示用户点击它可以弹出菜单,使按钮在界面上有符合功能需求的外观显示。
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 创建一个PopupMenu实例传入应用的上下文环境context和关联的按钮控件mButton这样创建的弹出式菜单会在点击mButton时弹出并且依赖于传入的上下文环境进行相关操作如资源加载等
mPopupMenu = new PopupMenu(context, mButton);
// 获取创建好的弹出式菜单mPopupMenu对应的菜单对象并赋值给成员变量mMenu通过这个对象后续可以对菜单中的具体菜单项进行操作如添加、查找等
mMenu = mPopupMenu.getMenu();
// 通过弹出式菜单mPopupMenu的getMenuInflater方法获取菜单填充器对象然后调用inflate方法传入菜单资源IDmenuId和菜单对象mMenu
// 这一步操作会根据指定的菜单资源ID对应的XML布局文件内容将菜单项填充到mMenu对象中从而构建出具有实际菜单项的菜单结构用于后续展示给用户选择操作。
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 为按钮mButton设置点击事件监听器当按钮被点击时会触发监听器中的onClick方法在这个方法内部通过调用mPopupMenu的show方法
// 实现弹出式菜单的显示,使得用户点击按钮后能看到弹出的菜单供其选择相应的菜单项,实现下拉菜单的基本交互功能。
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mPopupMenu.show();
}
});
}
// 用于设置弹出式菜单中菜单项点击事件监听器的方法接收一个OnMenuItemClickListener类型的参数listener
// 如果弹出式菜单mPopupMenu不为空就将传入的监听器设置给mPopupMenu使得当用户点击弹出菜单中的菜单项时会触发listener中定义的相应逻辑
// 以便执行根据菜单项选择而不同的业务操作(如跳转到不同页面、执行不同功能等)。
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu!= null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
// 用于在菜单mMenu中查找指定ID的菜单项的方法接收一个int类型的参数id表示要查找的菜单项的资源ID
// 通过调用mMenu对象的findItem方法根据传入的ID查找对应的菜单项并返回查找到的MenuItem对象方便后续对该菜单项进行进一步操作如获取其属性、设置其状态等
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
// 用于设置关联按钮mButton显示文本的方法接收一个CharSequence类型的参数title通过调用mButton的setText方法
// 将传入的文本设置为按钮上显示的内容,这样可以根据不同的业务场景动态改变按钮上显示的提示信息,增强交互的友好性和表意性。
public void setTitle(CharSequence title) {
mButton.setText(title);
}
}

@ -0,0 +1,121 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
* Apache License 2.0
*/
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 声明该类所属的包名表明这个类位于net.micode.notes.ui包下在项目的整体包结构中有其特定的组织和访问层级关系。
package net.micode.notes.ui;
// 导入必要的Android系统相关类Context用于获取应用的上下文环境信息是很多Android操作如资源获取、系统服务调用等的基础。
// Cursor用于处理数据库查询结果集通过它可以遍历查询返回的数据记录等操作。
import android.content.Context;
import android.database.Cursor;
// 导入View和ViewGroup相关类View是Android中所有可视化组件的基类ViewGroup是可以包含其他子视图的视图容器类用于构建复杂的UI布局结构。
import android.view.View;
import android.view.ViewGroup;
// 导入CursorAdapter类这是Android中用于将数据库查询结果Cursor与ListView等列表视图进行适配的抽象类子类需要实现特定方法来定制数据展示逻辑。
import android.widget.CursorAdapter;
// 导入LinearLayout类用于创建线性布局是一种常见的布局方式可以按照水平或垂直方向排列子视图。
import android.widget.LinearLayout;
// 导入TextView类用于在界面上展示文本信息是显示文字内容的常用组件。
import android.widget.TextView;
// 导入项目中自动生成的资源类R通过它可以访问项目中的各种资源如布局文件、图片资源、字符串资源等这里主要用于获取特定的字符串资源和布局文件资源。
// 同时导入了与数据相关的类Notes以及Notes类中的NoteColumns内部类用于处理笔记数据相关的操作比如获取数据列信息等。
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
// FoldersListAdapter类继承自CursorAdapter抽象类主要用于将数据库查询到的文件夹相关数据以Cursor形式表示适配到列表视图中进行展示
// 并且提供了获取文件夹名称以及定制列表项视图显示内容等功能方便在Android应用中展示文件夹列表信息。
public class FoldersListAdapter extends CursorAdapter {
// 定义一个字符串数组常量PROJECTION用于指定从数据库查询时需要获取的列信息这里只包含了NoteColumns.ID可能是文件夹的唯一标识符列和NoteColumns.SNIPPET可能是文件夹相关的简短描述等信息列
public static final String [] PROJECTION = {
NoteColumns.ID,
NoteColumns.SNIPPET
};
// 定义一个常量ID_COLUMN表示在PROJECTION数组中ID列的索引位置这里初始化为0方便后续通过索引快速获取对应列的数据。
public static final int ID_COLUMN = 0;
// 定义一个常量NAME_COLUMN表示在PROJECTION数组中用于显示名称的列这里实际对应NoteColumns.SNIPPET列的索引位置初始化为1用于后续按索引获取文件夹名称相关数据。
public static final int NAME_COLUMN = 1;
// FoldersListAdapter类的构造函数用于创建该适配器的实例接收一个Context类型的参数context用于获取应用的上下文环境信息
// 以及一个Cursor类型的参数c这个Cursor包含了从数据库查询出来的文件夹相关数据通过调用父类CursorAdapter的构造函数将这两个参数传递进去完成父类的初始化以及基础的适配设置工作。
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO Auto-generated constructor stub
// 这里的TODO注释通常表示此处还有待完善或者补充的代码逻辑可能后续需要根据具体业务需求进一步添加初始化相关的操作等内容。
}
// 重写CursorAdapter类中的newView方法该方法用于创建一个新的视图View对象用于在列表中展示每一项数据在这里就是每个文件夹对应的列表项视图
// 接收三个参数Context类型的context用于获取应用的上下文环境信息以便加载相关资源等Cursor类型的cursor虽然在这里未使用但在更复杂的场景下可以用于获取数据来初始化视图
// ViewGroup类型的parent表示该视图的父容器用于确定视图的布局等相关属性。在这里返回一个新创建的FolderListItem对象它是自定义的用于展示文件夹信息的列表项视图类。
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
// 重写CursorAdapter类中的bindView方法该方法用于将数据库查询到的数据通过Cursor表示绑定到已经创建好的视图View对象上实现数据在视图上的展示。
// 接收三个参数View类型的view表示要绑定数据的视图对象这里实际上就是前面newView方法创建的FolderListItem对象Context类型的context用于获取应用的上下文环境信息
// Cursor类型的cursor包含了要绑定的数据记录。在方法内部首先判断传入的视图是否是FolderListItem类型如果是则进行如下操作
// 获取文件夹的名称通过判断当前记录中ID列的值是否等于Notes.ID_ROOT_FOLDER可能是表示根文件夹的特定标识符来决定显示的名称
// 如果是根文件夹则获取R.string.menu_move_parent_folder对应的字符串资源作为名称可能是显示“上级文件夹”之类的特定文本否则获取NAME_COLUMN列对应的字符串作为文件夹名称。
// 最后调用((FolderListItem) view).bind(folderName)方法将获取到的文件夹名称传递给FolderListItem对象的bind方法用于在对应的视图上具体是TextView组件显示文件夹名称。
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof FolderListItem) {
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
((FolderListItem) view).bind(folderName);
}
}
// 定义一个方法用于获取指定位置的文件夹名称接收一个Context类型的参数context用于获取应用的上下文环境信息以及一个int类型的参数position表示在列表中的位置索引。
// 首先通过调用getItem方法继承自CursorAdapter类获取指定位置对应的Cursor对象包含了该位置对应的文件夹数据记录然后按照与bindView方法中类似的逻辑
// 判断该记录中ID列的值是否等于Notes.ID_ROOT_FOLDER来决定返回的文件夹名称是特定的根文件夹名称字符串从资源中获取还是从NAME_COLUMN列获取的普通文件夹名称字符串最终返回获取到的文件夹名称。
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position);
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
// 定义一个内部私有类FolderListItem它继承自LinearLayout用于表示文件夹列表中的每一个列表项视图内部包含了用于显示文件夹名称的TextView组件
// 并且提供了一个bind方法用于将文件夹名称数据绑定到TextView组件上进行显示实现了列表项视图的数据展示逻辑定制。
private class FolderListItem extends LinearLayout {
// 定义一个TextView类型的成员变量mName用于存储和显示文件夹的名称是列表项视图中展示文本信息的核心组件。
private TextView mName;
// FolderListItem类的构造函数用于创建该列表项视图的实例接收一个Context类型的参数context用于获取应用的上下文环境信息以便加载相关资源等。
// 首先调用父类LinearLayout的构造函数传入上下文环境context完成父类的初始化操作确保该列表项视图能继承LinearLayout的布局属性和功能。
// 然后通过inflate方法加载名为R.layout.folder_list_item的布局文件并将其填充到当前的FolderListItem实例中也就是this所代表的当前对象
// 这个布局文件应该包含了用于展示文件夹信息的各个子控件如这里的mName对应的TextView的布局定义使得这些控件能够在界面上显示出来。
// 最后通过findViewById方法在已加载的布局中查找ID为R.id.tv_folder_name的视图控件并将其转换为TextView类型赋值给mName成员变量以便后续可以操作这个TextView来显示文件夹名称。
public FolderListItem(Context context) {
super(context);
inflate(context, R.layout.folder_list_item, this);
mName = (TextView) findViewById(R.id.tv_folder_name);
}
// 定义一个bind方法用于将传入的文件夹名称数据绑定到TextView组件mName上进行显示接收一个String类型的参数name表示要显示的文件夹名称
// 通过调用mName的setText方法将传入的名称字符串设置为TextView的显示内容从而实现在列表项视图上展示文件夹名称的功能。
public void bind(String name) {
mName.setText(name);
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,310 @@
/*
* The MiCode Open Source CommunityApache License 2.0
* 使使
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 声明该类所在的包名属于net.micode.notes.ui包这有助于在项目中对类进行合理的组织和引用避免类名冲突等问题。
package net.micode.notes.ui;
// 导入Android系统的上下文相关类用于获取应用的各种资源、与系统服务交互等例如获取字符串资源、启动其他组件等操作都依赖于此。
import android.content.Context;
// 导入用于表示矩形区域的类,在处理视图的边界、触摸区域等方面可能会用到,比如确定某个触摸点是否在特定的矩形范围内等情况。
import android.graphics.Rect;
// 导入文本布局相关的类,可通过它获取文本在视图中的行信息、字符偏移量等内容,有助于进行基于文本布局的操作,比如根据触摸位置确定对应的文本位置。
import android.text.Layout;
// 导入文本选择相关的类,用于设置文本的选中区域,比如设置光标位置、选中一段文本等操作都可以通过它提供的方法来实现。
import android.text.Selection;
// 导入处理包含样式等复杂文本的接口用于判断文本是否包含特定样式如这里后续会涉及的URL样式等以及获取相关样式元素等操作。
import android.text.Spanned;
// 导入文本工具类,提供了一些便捷的文本处理方法,例如判断文本是否为空字符串等常用操作,方便代码中对文本状态的判断。
import android.text.TextUtils;
// 导入用于处理文本中URL样式的类通过它可以识别文本里的链接样式并进行相应的操作比如点击链接跳转等功能实现会用到它。
import android.text.style.URLSpan;
// 导入Android系统的工具类主要用于记录日志信息方便在开发调试阶段查看程序运行情况定位问题所在。
import android.util.AttributeSet;
import android.util.Log;
// 导入用于创建上下文菜单的类,使得该自定义视图可以弹出右键菜单等,便于提供更多与文本编辑相关的操作选项给用户。
import android.view.ContextMenu;
// 导入用于处理按键事件的类,能够捕获如键盘上按键按下、抬起等操作,进而实现基于按键操作的功能逻辑,例如按下回车键执行特定操作等。
import android.view.KeyEvent;
// 导入菜单项相关的类,用于定义和操作上下文菜单中的具体条目,比如设置菜单项的显示文本、点击监听器等。
import android.view.MenuItem;
// 导入菜单项点击监听器相关的接口,用于处理菜单项被点击时的具体业务逻辑,实现点击菜单项后执行相应功能的效果。
import android.view.MenuItem.OnMenuItemClickListener;
// 导入用于处理触摸事件的类,能够捕获视图上的触摸操作,像触摸按下、滑动、抬起等动作,进而基于这些动作实现对应的交互逻辑。
import android.view.MotionEvent;
// 导入Android系统默认的文本编辑框类本自定义的NoteEditText类继承自它以在其基础上扩展更多符合特定需求的功能。
import android.widget.EditText;
// 导入项目自定义的资源相关类通过它可以获取在项目中定义的各种资源比如字符串资源这里后续用于获取不同链接类型对应的提示文本资源ID等
import net.micode.notes.R;
// 导入Java中的HashMap类和Map接口用于创建键值对形式的集合这里用来存储不同链接协议与对应的资源ID的映射关系方便查找使用。
import java.util.HashMap;
import java.util.Map;
// 自定义的文本编辑框类继承自Android系统的EditText类目的是在原生文本编辑框功能基础上进行定制化扩展以满足特定应用场景可能是笔记应用中编辑文本的特殊需求的功能要求。
public class NoteEditText extends EditText {
// 定义一个静态的、用于日志记录的标签字符串常量,其值为"NoteEditText"在使用Log输出日志时通过这个标签可以方便地识别出是该类中产生的日志信息便于调试和问题排查。
private static final String TAG = "NoteEditText";
// 用于记录当前这个NoteEditText实例在一组编辑文本框中的索引位置例如在多个编辑文本框组成的列表里通过该索引可以区分不同的文本框方便进行相关操作和管理。
private int mIndex;
// 用于记录在删除操作之前,文本选择区域的起始位置,后续在处理删除相关逻辑时,可以依据这个位置来判断是否满足删除当前文本框等条件,辅助进行更精准的操作判断。
private int mSelectionStartBeforeDelete;
// 定义表示电话链接协议的常量字符串,其值为"tel:",用于在文本中识别是否存在电话链接相关内容,以便后续进行相应的处理(如根据链接类型展示不同提示等)。
private static final String SCHEME_TEL = "tel:" ;
// 定义表示HTTP网页链接协议的常量字符串其值为"http:",用于判断文本里是否包含网页链接,进而采取对应的操作逻辑,比如点击链接跳转到网页等。
private static final String SCHEME_HTTP = "http:" ;
// 定义表示邮件链接协议的常量字符串,其值为"mailto:",用于识别文本中的邮件链接情况,方便实现点击邮件链接启动邮件客户端等相关功能。
private static final String SCHEME_EMAIL = "mailto:" ;
// 创建一个静态的HashMap类型的集合用于存储不同链接协议如上述的tel、http、mailto等与对应的资源ID这些资源ID通常关联着在界面上显示的对应链接类型的提示字符串等资源之间的映射关系方便后续根据链接协议快速查找并获取相应的资源显示信息。
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
// 静态代码块用于在类加载时初始化上面定义的sSchemaActionResMap集合将不同链接协议与对应的资源ID进行关联赋值使得后续可以直接通过协议字符串查找到对应的资源ID例如tel协议对应R.string.note_link_tel这个资源ID以此类推。
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
// 定义一个内部接口名为OnTextViewChangeListener用于作为文本编辑框文本变化的监听器规定了在特定文本编辑操作发生时如删除、新增文本、文本内容变化等情况需要外部实现的回调方法
// 这样外部类比如包含该编辑文本框的Activity等可以通过实现这个接口并将实现对象设置给该编辑文本框来监听并处理相应的文本变化逻辑实现业务上的解耦和功能扩展。
/**
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
public interface OnTextViewChangeListener {
/**
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
*/
// 定义当按下删除键对应KeyEvent类中的KEYCODE_DEL常量并且当前文本编辑框中的文本内容为空时需要调用的方法
// 外部实现该接口的类需要根据具体业务场景来编写这个方法的具体逻辑,比如在笔记应用中可能是从编辑文本框列表里移除当前这个空文本框等操作。
void onEditTextDelete(int index, String text);
/**
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
*/
// 定义当按下回车键对应KeyEvent类中的KEYCODE_ENTER常量时需要调用的方法外部实现该接口的类要根据业务需求来实现具体的添加文本框逻辑
// 例如在笔记应用中可能是创建一个新的编辑文本框并添加到合适的位置(通常是当前文本框之后),方便用户继续输入内容等操作。
void onEditTextEnter(int index, String text);
/**
* Hide or show item option when text change
*/
// 定义当文本内容发生变化时需要调用的方法,外部实现该接口的类要依据文本是否有内容等情况来决定相关操作选项(比如上下文菜单里的某些选项、界面上某些与文本相关的按钮等)的显示或隐藏状态,以提供符合当前文本状态的交互界面。
void onTextChange(int index, boolean hasText);
}
// 定义一个成员变量用于存储实现了OnTextViewChangeListener接口的监听器对象通过调用setOnTextViewChangeListener方法可以将外部实现的监听器对象赋值给它
// 从而实现文本编辑框文本变化事件与外部处理逻辑的关联,让外部类能够监听并处理文本编辑框的各种文本变化相关情况。
private OnTextViewChangeListener mOnTextViewChangeListener;
// 构造函数接收一个Context类型的参数用于创建NoteEditText实例时传入上下文信息调用父类EditText的构造函数传入上下文和null作为属性集通常在简单创建实例且不需要从XML布局中加载属性时使用这种方式
// 同时初始化当前编辑文本框的索引值mIndex为0表示默认的初始索引位置。
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
// 设置当前编辑文本框索引值的方法外部可以通过调用该方法传入新的索引值来更新mIndex变量方便在多个编辑文本框组成的场景中准确地对每个文本框进行定位和管理例如在列表中调整文本框顺序等操作时会用到。
public void setIndex(int index) {
mIndex = index;
}
// 设置文本变化监听器的方法外部类如包含该编辑文本框的Activity等可以通过调用该方法传入实现了OnTextViewChangeListener接口的对象将该对象赋值给mOnTextViewChangeListener变量
// 这样当文本编辑框发生文本删除、新增、内容变化等相关事件时,就能触发对应的回调方法,执行外部类中定义的相应业务逻辑了。
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 构造函数接收上下文对象Context和属性集AttributeSet作为参数调用父类EditText的构造函数传入上下文、属性集以及默认的编辑文本框样式通过android.R.attr.editTextStyle指定
// 这种构造方式常用于在XML布局文件中定义该自定义编辑文本框时加载XML中配置的属性并应用默认样式来初始化文本框实例使其具有合适的外观和初始属性设置。
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
// 构造函数接收上下文对象Context、属性集AttributeSet和默认样式定义defStyle作为参数调用父类EditText的构造函数传入相应参数进行初始化
// 不过当前构造函数中的具体实现部分是自动生成的占位代码由TODO注释标识意味着后续可能需要根据具体业务需求进一步完善此处的初始化逻辑比如根据传入的defStyle进行更多样式相关的设置等
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
// 重写父类EditText的onTouchEvent方法用于处理该编辑文本框上的触摸操作相关逻辑通过这个方法可以实现根据用户触摸位置来进行文本选择等交互功能提升用户在文本编辑时的操作体验。
@Override
public boolean onTouchEvent(MotionEvent event) {
// 根据触摸事件的动作类型通过event.getAction()方法获取进行不同的处理逻辑分支这里主要关注MotionEvent.ACTION_DOWN即触摸按下动作的情况
// 对于其他触摸动作类型如触摸滑动、触摸抬起等则默认按照父类EditText原有的处理逻辑执行通过调用super.onTouchEvent(event)来实现),这样可以在扩展特定触摸动作逻辑的同时,保留父类已有的触摸处理功能。
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸点相对于该编辑文本框左上角的横坐标位置(以像素为单位),这个坐标是在视图坐标系下的原始横坐标值,后续需要进行一些调整来得到相对于文本内容区域的准确横坐标。
int x = (int) event.getX();
// 获取触摸点相对于该编辑文本框左上角的纵坐标位置(以像素为单位),同样这是视图坐标系下的原始纵坐标值,后续也要进行相应调整来确定在文本内容区域内的准确位置。
int y = (int) event.getY();
// 将获取到的横坐标减去文本框的左边总内边距(包括内边距、边框等占据的空间),得到触摸点相对于文本内容区域左边的横坐标位置,使得坐标能准确对应到文本内容所在的实际可编辑区域内。
x -= getTotalPaddingLeft();
// 将获取到的纵坐标减去文本框的上边总内边距,得到触摸点相对于文本内容区域上边的纵坐标位置,确保坐标与文本内容区域的实际布局相对应,方便后续根据坐标来定位文本位置。
y -= getTotalPaddingTop();
// 考虑到文本内容在文本框内可能存在滚动情况(比如文本内容较多,出现了上下滚动条),需要将触摸点的横坐标加上当前文本框的水平滚动偏移量,
// 这样得到的横坐标才是在实际完整文本内容布局中的准确横坐标位置,确保触摸位置能准确对应到文本中的字符位置,无论文本是否有滚动。
x += getScrollX();
// 同理,将触摸点的纵坐标加上当前文本框的垂直滚动偏移量,得到在实际完整文本内容布局中的准确纵坐标位置,使得触摸位置能精准对应到文本中的具体行、字符位置等。
y += getScrollY();
// 获取当前编辑文本框中文本的布局信息对象,通过这个对象可以获取文本在文本框内是如何分行、每行的字符范围等详细布局情况,为后续根据触摸坐标确定具体的文本字符位置提供基础数据。
Layout layout = getLayout();
// 根据经过调整后的触摸点纵坐标y通过布局对象的getLineForVertical方法获取触摸点所在的文本行索引即确定触摸位置处于文本的第几行方便后续进一步定位该行内的具体字符位置。
int line = layout.getLineForVertical(y);
// 根据触摸点所在的文本行索引line以及经过调整后的横坐标x通过布局对象的getOffsetForHorizontal方法获取触摸点对应的文本字符在整个文本字符串中的偏移量也就是字符位置索引
// 这个偏移量能够准确地指出触摸位置对应的是文本中的哪个字符,为后续设置文本选中区域提供准确的位置信息。
int off = layout.getOffsetForHorizontal(line, x);
// 通过Selection类的setSelection方法依据获取到的字符偏移量off来设置文本的选中区域即将文本光标定位到触摸点对应的字符位置或者如果有长按等操作逻辑也可以实现选中从触摸点开始的一段文本等效果
// 方便用户后续进行复制、删除、粘贴等文本编辑操作,提升文本编辑的交互性和便捷性。
Selection.setSelection(getText(), off);
break;
}
// 返回调用父类EditText的onTouchEvent方法的结果这样既保证了在处理完特定触摸动作如这里的ACTION_DOWN的自定义逻辑后还能让父类继续处理触摸事件相关的其他默认逻辑
// 例如触摸事件的传递、与系统其他组件的交互等功能,确保整个触摸事件处理流程的完整性和正确性,避免影响其他相关的触摸操作功能。
return super.onTouchEvent(event);
}
@Override
// 重写onKeyDown方法用于处理按键按下的事件
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 当按下回车键时如果存在文本视图内容改变的监听器mOnTextViewChangeListener不为空
if (mOnTextViewChangeListener!= null) {
// 直接返回false这里可能是根据具体业务逻辑决定不做额外处理直接向上传递该返回值
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 当按下删除键DEL记录当前删除操作前的文本选择起始位置以便后续可能的业务逻辑使用
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
// 如果上述case中没有处理或者需要默认行为调用父类的onKeyDown方法继续处理
return super.onKeyDown(keyCode, event);
}
@Override
// 重写onKeyUp方法用于处理按键抬起的事件
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
// 当删除键DEL抬起时如果文本视图内容改变的监听器mOnTextViewChangeListener不为空
if (mOnTextViewChangeListener!= null) {
// 判断如果当前选择起始位置为0 并且 mIndex不等于0这里mIndex具体含义需看上下文可能是某种索引标识
if (0 == mSelectionStartBeforeDelete && mIndex!= 0) {
// 调用监听器的onEditTextDelete方法传递当前索引mIndex以及文本内容通过getText().toString()获取并返回true表示已经处理该事件
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
// 如果监听器为空,打印日志提示监听器未设置
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
// 当回车键抬起时如果文本视图内容改变的监听器mOnTextViewChangeListener不为空
if (mOnTextViewChangeListener!= null) {
// 获取当前文本选择的起始位置
int selectionStart = getSelectionStart();
// 获取从选择起始位置到文本末尾的子字符串,即回车键之后的文本内容
String text = getText().subSequence(selectionStart, getText().length()).toString();
// 将文本内容设置为从开头到选择起始位置的子字符串,相当于删除了回车键之后的文本内容
setText(getText().subSequence(0, selectionStart));
// 调用监听器的onEditTextEnter方法传递下一个索引mIndex + 1以及回车键之后的文本内容text
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
// 如果监听器为空,打印日志提示监听器未设置
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
// 如果上述case中没有处理或者需要默认行为调用父类的onKeyUp方法继续处理
return super.onKeyUp(keyCode, event);
}
@Override
// 重写onFocusChanged方法用于处理焦点改变的事件
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
// 如果文本视图内容改变的监听器mOnTextViewChangeListener不为空
if (mOnTextViewChangeListener!= null) {
// 如果当前失去焦点(!focused并且文本内容为空TextUtils.isEmpty(getText())
if (!focused && TextUtils.isEmpty(getText())) {
// 调用监听器的onTextChange方法传递当前索引mIndex以及表示文本为空的布尔值false
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
// 如果不是上述情况即获得焦点或者文本不为空调用监听器的onTextChange方法传递当前索引mIndex以及表示文本不为空的布尔值true
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
// 调用父类的onFocusChanged方法继续处理焦点改变相关的其他默认逻辑
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
@Override
// 重写onCreateContextMenu方法用于创建上下文菜单通常是长按文本等操作弹出的菜单
protected void onCreateContextMenu(ContextMenu menu) {
// 如果文本内容是Spanned类型Spanned通常用于包含富文本信息比如包含链接等格式的文本
if (getText() instanceof Spanned) {
// 获取当前文本选择的起始位置
int selStart = getSelectionStart();
// 获取当前文本选择的结束位置
int selEnd = getSelectionEnd();
// 获取选择范围的最小值(起始和结束位置中较小的那个)
int min = Math.min(selStart, selEnd);
// 获取选择范围的最大值(起始和结束位置中较大的那个)
int max = Math.max(selStart, selEnd);
// 获取在选择范围内的所有URLSpan类型的对象数组URLSpan通常用于表示文本中的链接信息
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
int defaultResId = 0;
// 遍历sSchemaActionResMap的键集合这里sSchemaActionResMap具体含义需看上下文可能是某种资源映射关系
for (String schema : sSchemaActionResMap.keySet()) {
// 如果URLSpan中的URL包含当前遍历到的模式schema
if (urls[0].getURL().indexOf(schema) >= 0) {
// 获取对应的资源ID
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
if (defaultResId == 0) {
// 如果没有找到匹配的资源ID设置默认的资源ID这里是R.string.note_link_other具体字符串资源需看项目中的定义
defaultResId = R.string.note_link_other;
}
// 向上下文菜单中添加一个菜单项参数依次为组ID0、菜单项ID0、排序顺序0、显示的字符串资源IDdefaultResId
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 当点击该菜单项时调用URLSpan的onClick方法传入当前NoteEditText实例this通常用于触发链接跳转等操作
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
// 调用父类的onCreateContextMenu方法继续处理上下文菜单创建的其他默认逻辑
super.onCreateContextMenu(menu);
}

@ -0,0 +1,300 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 所在包名表明该类属于net.micode.notes.ui包下用于UI相关的功能逻辑推测
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
// 导入与数据相关的类,可能用于操作笔记数据等功能
import net.micode.notes.data.Contact;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
// NoteItemData类可能用于封装笔记相关的数据项方便在UI层进行展示和操作等
public class NoteItemData {
// 定义一个字符串数组用于指定从数据库查询笔记数据时要获取的列名对应Notes表中的各个字段
static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE,
NoteColumns.HAS_ATTACHMENT,
NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
};
// 以下是各个列在PROJECTION数组中的索引位置方便后续从Cursor中获取对应的值
private static final int ID_COLUMN = 0;
private static final int ALERTED_DATE_COLUMN = 1;
private static final int BG_COLOR_ID_COLUMN = 2;
private static final int CREATED_DATE_COLUMN = 3;
private static final int HAS_ATTACHMENT_COLUMN = 4;
private static final int MODIFIED_DATE_COLUMN = 5;
private static final int NOTES_COUNT_COLUMN = 6;
private static final int PARENT_ID_COLUMN = 7;
private static final int SNIPPET_COLUMN = 8;
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
// 笔记的唯一标识ID
private long mId;
// 提醒日期(可能是设置了提醒功能的笔记对应的提醒时间戳之类的)
private long mAlertDate;
// 背景颜色的ID用于设置笔记展示的背景颜色推测
private int mBgColorId;
// 笔记创建日期的时间戳
private long mCreatedDate;
// 表示笔记是否有附件true表示有附件false表示没有
private boolean mHasAttachment;
// 笔记最后修改日期的时间戳
private long mModifiedDate;
// 笔记相关的数量(具体含义需结合业务,可能是子笔记数量之类的)
private int mNotesCount;
// 父级ID可能表示该笔记所属的文件夹等的ID
private long mParentId;
// 笔记的摘要内容,简短描述笔记的文字信息
private String mSnippet;
// 笔记的类型可能有不同分类如普通笔记、系统笔记等具体要看Notes类中定义的类型常量
private int mType;
// 部件Widget的ID可能和在桌面展示相关的部件有关如果有此功能的话
private int mWidgetId;
// 部件Widget的类型同样和桌面部件相关推测
private int mWidgetType;
// 联系人姓名(如果笔记和联系人相关,比如通话记录对应的笔记等情况)
private String mName;
// 电话号码(如果笔记和通话记录等相关)
private String mPhoneNumber;
// 以下几个布尔变量用于标记笔记在列表中的位置相关的状态
private boolean mIsLastItem;
private boolean mIsFirstItem;
private boolean mIsOnlyOneItem;
private boolean mIsOneNoteFollowingFolder;
private boolean mIsMultiNotesFollowingFolder;
// 构造函数用于根据传入的Context和Cursor来初始化NoteItemData对象的各个属性
public NoteItemData(Context context, Cursor cursor) {
// 从Cursor中获取笔记的ID并赋值给mId属性
mId = cursor.getLong(ID_COLUMN);
// 从Cursor中获取提醒日期并赋值给mAlertDate属性
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
// 从Cursor中获取背景颜色ID并赋值给mBgColorId属性
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
// 从Cursor中获取创建日期并赋值给mCreatedDate属性
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
// 根据从Cursor中获取的是否有附件的标识整数转换为布尔值赋值给mHasAttachment属性
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0)? true : false;
// 从Cursor中获取修改日期并赋值给mModifiedDate属性
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
// 从Cursor中获取笔记数量并赋值给mNotesCount属性
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
// 从Cursor中获取父级ID并赋值给mParentId属性
mParentId = cursor.getLong(PARENT_ID_COLUMN);
// 从Cursor中获取笔记摘要内容并赋值给mSnippet属性同时去除一些特定的标记TAG_CHECKED和TAG_UNCHECKED具体含义需看NoteEditActivity类中的定义
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
// 从Cursor中获取笔记类型并赋值给mType属性
mType = cursor.getInt(TYPE_COLUMN);
// 从Cursor中获取部件ID并赋值给mWidgetId属性
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
// 从Cursor中获取部件类型并赋值给mWidgetType属性
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 初始化电话号码为空字符串
mPhoneNumber = "";
// 如果父级ID是通话记录文件夹的IDNotes.ID_CALL_RECORD_FOLDER具体值需看Notes类定义
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 通过DataUtils工具类根据笔记ID从ContentResolver中获取电话号码并赋值给mPhoneNumber属性
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
// 如果获取到的电话号码不为空
if (!TextUtils.isEmpty(mPhoneNumber)) {
// 通过Contact类的静态方法根据电话号码获取联系人姓名并赋值给mName属性
mName = Contact.getContact(context, mPhoneNumber);
// 如果获取联系人姓名失败返回null则将电话号码作为姓名
if (mName == null) {
mName = mPhoneNumber;
}
}
}
// 如果联系人姓名还是为null可能前面获取过程出现问题等情况则设置为空字符串
if (mName == null) {
mName = "";
}
// 调用checkPostion方法来检查该笔记在列表中的位置相关状态
checkPostion(cursor);
}
// 私有方法用于检查该笔记在列表中的位置相关状态通过Cursor中的信息来判断
private void checkPostion(Cursor cursor) {
// 判断是否是列表中的最后一项根据Cursor的isLast方法结果赋值给mIsLastItem属性
mIsLastItem = cursor.isLast()? true : false;
// 判断是否是列表中的第一项根据Cursor的isFirst方法结果赋值给mIsFirstItem属性
mIsFirstItem = cursor.isFirst()? true : false;
// 判断是否列表中只有一项通过比较Cursor中的记录数量是否为1来赋值给mIsOnlyOneItem属性
mIsOnlyOneItem = (cursor.getCount() == 1);
// 初始化是否有多条笔记跟随在文件夹后的标记为false
mIsMultiNotesFollowingFolder = false;
// 初始化是否有一条笔记跟随在文件夹后的标记为false
mIsOneNoteFollowingFolder = false;
// 如果笔记类型是普通笔记Notes.TYPE_NOTE并且不是列表中的第一项
if (mType == Notes.TYPE_NOTE &&!mIsFirstItem) {
// 获取当前Cursor的位置索引
int position = cursor.getPosition();
// 将Cursor移动到前一条记录上一条数据
if (cursor.moveToPrevious()) {
// 如果前一条记录的类型是文件夹类型Notes.TYPE_FOLDER或者系统类型Notes.TYPE_SYSTEM
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
// 判断如果总记录数大于当前位置索引加1说明有多条笔记跟随在文件夹后
if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true;
} else {
// 否则说明只有一条笔记跟随在文件夹后
mIsOneNoteFollowingFolder = true;
}
}
// 将Cursor再移回原来的下一条记录位置保证Cursor位置状态的正确避免影响后续操作
if (!cursor.moveToNext()) {
// 如果无法移回抛出异常表示出现了不正常的Cursor移动情况
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
// 判断是否有一条笔记跟随在文件夹后,对外提供的访问方法
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
// 判断是否有多条笔记跟随在文件夹后,对外提供的访问方法
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
// 判断是否是列表中的最后一项,对外提供的访问方法
public boolean isLast() {
return mIsLastItem;
}
// 获取联系人姓名(如果有相关的联系人信息),对外提供的访问方法
public String getCallName() {
return mName;
}
// 判断是否是列表中的第一项,对外提供的访问方法
public boolean isFirst() {
return mIsFirstItem;
}
// 判断是否列表中只有一项,对外提供的访问方法
public boolean isSingle() {
return mIsOnlyOneItem;
}
// 获取笔记的唯一标识ID对外提供的访问方法
public long getId() {
return mId;
}
// 获取提醒日期,对外提供的访问方法
public long getAlertDate() {
return mAlertDate;
}
// 获取创建日期,对外提供的访问方法
public long getCreatedDate() {
return mCreatedDate;
}
// 判断笔记是否有附件,对外提供的访问方法
public boolean hasAttachment() {
return mHasAttachment;
}
// 获取修改日期,对外提供的访问方法
public long getModifiedDate() {
return mModifiedDate;
}
// 获取背景颜色ID对外提供的访问方法
public int getBgColorId() {
return mBgColorId;
}
// 获取父级ID对外提供的访问方法
public long getParentId() {
return mParentId;
}
// 获取笔记数量,对外提供的访问方法
public int getNotesCount() {
return mNotesCount;
}
// 获取文件夹ID这里返回的就是父级ID可能是为了语义更明确表示是文件夹相关的ID对外提供的访问方法
public long getFolderId () {
return mParentId;
}
// 获取笔记类型,对外提供的访问方法
public int getType() {
return mType;
}
// 获取部件类型,对外提供的访问方法
public int getWidgetType() {
return mWidgetType;
}
// 获取部件ID对外提供的访问方法
public int getWidgetId() {
return mWidgetId;
}
// 获取笔记摘要内容,对外提供的访问方法
public String getSnippet() {
return mSnippet;
}
// 判断笔记是否有提醒设置根据提醒日期是否大于0来判断对外提供的访问方法
public boolean hasAlert() {
return (mAlertDate > 0);
}
// 判断是否是通话记录相关的笔记根据父级ID是否是通话记录文件夹ID并且电话号码不为空来判断对外提供的访问方法
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER &&!TextUtils.isEmpty(mPhoneNumber));
}
// 静态方法用于从Cursor中获取笔记类型方便在其他地方直接调用获取类型信息
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,213 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明,表明该类所属的包名,用于在项目中进行类的组织和管理
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import net.micode.notes.data.Notes;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
// NotesListAdapter类继承自CursorAdapter用于适配数据库游标数据到视图以便在列表等控件中展示笔记相关的数据
public class NotesListAdapter extends CursorAdapter {
// 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息
private static final String TAG = "NotesListAdapter";
// 保存上下文信息,用于后续创建视图、获取资源等操作
private Context mContext;
// 使用HashMap来记录每个位置索引对应的选中状态键为位置索引整数值为是否选中布尔值
private HashMap<Integer, Boolean> mSelectedIndex;
// 记录笔记的数量,用于一些判断和统计相关操作
private int mNotesCount;
// 表示当前是否处于选择模式例如多选模式等true表示处于选择模式false表示不在选择模式
private boolean mChoiceMode;
// 内部静态类用于封装桌面小部件App Widget的相关属性这里包含小部件的ID和类型两个属性
public static class AppWidgetAttribute {
public int widgetId;
public int widgetType;
};
// 构造函数用于初始化NotesListAdapter实例
public NotesListAdapter(Context context) {
// 调用父类CursorAdapter的构造函数传入上下文和初始游标这里传入null可能后续会通过其他方法设置游标
super(context, null);
// 初始化mSelectedIndex创建一个新的HashMap用于记录选中状态
mSelectedIndex = new HashMap<Integer, Boolean>();
// 保存传入的上下文信息
mContext = context;
// 初始化笔记数量为0
mNotesCount = 0;
}
// 该方法用于创建一个新的视图View在列表中每个数据项对应的视图需要通过此方法创建这里返回一个NotesListItem类型的新视图实例传入当前上下文作为参数来创建视图
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context);
}
// 该方法用于将游标中的数据绑定到已创建的视图上,使得视图能够展示对应的数据内容
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 如果视图是NotesListItem类型的实例
if (view instanceof NotesListItem) {
// 根据传入的上下文和游标数据创建一个NoteItemData实例用于获取和解析具体的数据信息
NoteItemData itemData = new NoteItemData(context, cursor);
// 调用NotesListItem的bind方法将相关数据和状态传递给视图进行绑定展示包括上下文、笔记数据项、是否处于选择模式以及当前位置对应的选中状态等信息
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
}
}
// 用于设置指定位置的选中状态将指定位置和对应的选中状态存入mSelectedIndex中并通知数据集已发生改变使得视图能够根据新的选中状态进行更新显示
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
// 用于判断当前是否处于选择模式返回mChoiceMode的值true表示处于选择模式false表示不在选择模式
public boolean isInChoiceMode() {
return mChoiceMode;
}
// 用于设置选择模式的开关当设置为选择模式时传入参数mode为true先清空之前的选中状态记录mSelectedIndex然后更新mChoiceMode的值
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
mChoiceMode = mode;
}
// 用于全选或全不选所有笔记数据项根据传入的checked参数决定遍历游标中的所有数据项对于类型为笔记Notes.TYPE_NOTE的数据项调用setCheckedItem方法设置其选中状态
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
if (cursor.moveToPosition(i)) {
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
setCheckedItem(i, checked);
}
}
}
}
// 用于获取所有已选中的数据项的ID集合创建一个HashSet用于存储Long类型的ID遍历mSelectedIndex中所有已选中的位置通过getItemId方法获取对应的数据项ID并添加到HashSet中同时对根文件夹IDNotes.ID_ROOT_FOLDER进行特殊判断和日志记录如果出现则认为是错误情况
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
}
}
}
return itemSet;
}
// 用于获取所有已选中的桌面小部件App Widget相关属性的集合创建一个HashSet用于存储AppWidgetAttribute实例遍历mSelectedIndex中所有已选中的位置通过getItem方法获取对应的数据项游标Cursor然后从中解析出小部件的ID和类型信息封装成AppWidgetAttribute实例添加到HashSet中同时对无效游标情况进行日志记录和错误处理如果游标为null则返回null
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Cursor c = (Cursor) getItem(position);
if (c!= null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
/**
* Don't close cursor here, only the adapter could close it
*/
} else {
Log.e(TAG, "Invalid cursor");
return null;
}
}
}
return itemSet;
}
// 用于获取已选中的数据项的数量获取mSelectedIndex中所有值布尔值表示选中状态的集合通过迭代器遍历该集合统计值为true即选中的数量并返回
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
Iterator<Boolean> iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
if (true == iter.next()) {
count++;
}
}
return count;
}
// 用于判断是否所有的数据项都已被选中通过比较已选中的数据项数量getSelectedCount方法获取和总笔记数量mNotesCount来判断当已选中数量不为0且等于总笔记数量时返回true表示全选状态否则返回false
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount!= 0 && checkedCount == mNotesCount);
}
// 用于判断指定位置的数据项是否被选中根据mSelectedIndex中对应位置的键值布尔值来判断如果对应位置的值为null则返回false表示未选中否则返回该位置对应的实际选中状态值
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
}
return mSelectedIndex.get(position);
}
// 当数据集内容发生改变时触发的回调方法这里先调用父类的onContentChanged方法执行默认操作然后调用calcNotesCount方法重新计算笔记的数量
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount();
}
// 当游标Cursor发生改变时触发的回调方法这里先调用父类的changeCursor方法执行默认操作然后调用calcNotesCount方法重新计算笔记的数量
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount();
}
// 私有方法用于计算笔记的数量先将mNotesCount重置为0然后遍历所有数据项通过getCount方法获取数量获取对应位置的数据项游标判断其数据类型是否为笔记Notes.TYPE_NOTE如果是则数量加1同时对无效游标情况进行日志记录和错误处理如果游标为null则直接返回
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c!= null) {
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
Log.e(TAG, "Invalid cursor");
return;
}
}
}
}

@ -0,0 +1,173 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明,表明该类所属的包名,用于在项目中进行类的组织和管理
package net.micode.notes.ui;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
// NotesListItem类继承自LinearLayout意味着它可以作为一个线性布局容器用于组织和展示笔记相关的各个子视图组件呈现出特定的笔记列表项样式
public class NotesListItem extends LinearLayout {
// 用于显示提醒相关图标的ImageView比如可能用于显示闹钟图标表示有提醒设置等情况
private ImageView mAlert;
// 用于显示笔记标题的TextView
private TextView mTitle;
// 用于显示时间相关信息如修改时间等的TextView
private TextView mTime;
// 用于显示通话名称相关信息的TextView可能在特定与通话记录相关的笔记场景下使用
private TextView mCallName;
// 保存当前笔记列表项对应的笔记数据对象,用于后续获取和展示各项数据
private NoteItemData mItemData;
// 用于多选等操作时显示的复选框CheckBox用于标记该项是否被选中
private CheckBox mCheckBox;
// 构造函数用于初始化NotesListItem实例传入上下文信息用于获取资源等操作
public NotesListItem(Context context) {
super(context);
// 通过inflate方法将布局资源R.layout.note_item加载到当前的LinearLayout中使得该类能够展示对应的布局样式
inflate(context, R.layout.note_item, this);
// 通过findViewById方法从加载的布局中找到对应的视图组件以便后续操作和设置数据
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
// 用于将数据绑定到视图组件上,根据传入的不同数据和状态参数来设置各个视图组件的显示内容和可见性等属性
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
// 如果处于选择模式choiceMode为true并且数据类型是笔记Notes.TYPE_NOTE
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
// 将复选框设置为可见,以便用户可以看到并操作选中状态
mCheckBox.setVisibility(View.VISIBLE);
// 根据传入的checked参数设置复选框的选中状态
mCheckBox.setChecked(checked);
} else {
// 如果不满足上述条件,则将复选框隐藏起来
mCheckBox.setVisibility(View.GONE);
}
// 保存传入的笔记数据对象,方便后续获取数据进行展示等操作
mItemData = data;
// 如果数据项的ID是通话记录文件夹的IDNotes.ID_CALL_RECORD_FOLDER
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
// 隐藏通话名称相关的TextView因为在通话记录文件夹情况下可能不需要显示这个信息
mCallName.setVisibility(View.GONE);
// 将提醒图标设置为可见,可能用于表示该文件夹有特殊的提醒相关属性或者状态
mAlert.setVisibility(View.VISIBLE);
// 设置标题的文本外观样式通过资源ID指定R.style.TextAppearancePrimaryItem
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
// 设置标题的文本内容,拼接了固定的通话记录文件夹名称字符串和根据数据获取的文件夹内文件数量信息(通过格式化字符串的方式展示)
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
// 设置提醒图标的资源图片这里使用通话记录相关的图标R.drawable.call_record
mAlert.setImageResource(R.drawable.call_record);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
// 如果数据项的父ID是通话记录文件夹的ID说明该数据项可能是通话记录文件夹下的具体内容
mCallName.setVisibility(View.VISIBLE);
// 设置通话名称TextView的文本内容为数据中获取的通话名称
mCallName.setText(data.getCallName());
// 设置标题的文本外观样式通过资源ID指定R.style.TextAppearanceSecondaryItem
mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem);
// 设置标题的文本内容为格式化后的摘要信息通过DataUtils工具类的方法进行格式化
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
// 如果数据项有提醒设置通过hasAlert方法判断
if (data.hasAlert()) {
// 设置提醒图标的资源图片为闹钟图标R.drawable.clock并将其设置为可见
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
// 如果没有提醒设置,则隐藏提醒图标
mAlert.setVisibility(View.GONE);
}
} else {
// 对于其他情况(既不是通话记录文件夹本身,也不是其下的具体内容)
mCallName.setVisibility(View.GONE);
// 设置标题的文本外观样式通过资源ID指定R.style.TextAppearancePrimaryItem
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
// 如果数据类型是文件夹Notes.TYPE_FOLDER
if (data.getType() == Notes.TYPE_FOLDER) {
// 设置标题的文本内容为文件夹的摘要信息加上文件夹内文件数量信息(通过格式化字符串的方式展示)
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
// 隐藏提醒图标,因为文件夹可能不需要显示提醒相关图标(具体看业务需求)
mAlert.setVisibility(View.GONE);
} else {
// 如果数据类型是普通笔记(不是文件夹)
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
// 如果数据项有提醒设置通过hasAlert方法判断
if (data.hasAlert()) {
// 设置提醒图标的资源图片为闹钟图标R.drawable.clock并将其设置为可见
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
// 如果没有提醒设置,则隐藏提醒图标
mAlert.setVisibility(View.GONE);
}
}
}
// 设置时间TextView的文本内容通过DateUtils工具类的方法将数据中的修改时间转换为相对时间格式例如“几分钟前”“昨天”等相对时间表述进行展示
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 调用setBackground方法根据数据设置当前列表项的背景样式具体的背景设置逻辑在该方法中实现
setBackground(data);
}
// 私有方法,用于根据笔记数据来设置当前列表项的背景资源,根据数据类型(是否为笔记以及笔记的不同状态等)来选择不同的背景资源进行设置
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
// 如果数据类型是笔记Notes.TYPE_NOTE
if (data.getType() == Notes.TYPE_NOTE) {
// 如果笔记是单独的isSingle方法判断或者是某个文件夹下单独跟随的isOneFollowingFolder方法判断
if (data.isSingle() || data.isOneFollowingFolder()) {
// 设置背景资源为单个笔记对应的背景资源通过NoteItemBgResources工具类的方法根据颜色ID获取
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
// 如果笔记是所在分组的最后一个isLast方法判断
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
// 如果笔记是所在分组的第一个isFirst方法判断或者是某个文件夹下多个跟随的isMultiFollowingFolder方法判断
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
// 其他情况普通的笔记在中间位置等设置背景资源为普通笔记对应的背景资源通过NoteItemBgResources工具类的方法根据颜色ID获取
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
} else {
// 如果数据类型不是笔记比如是文件夹则设置背景资源为文件夹对应的背景资源通过NoteItemBgResources工具类的方法获取
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
// 用于获取当前列表项对应的笔记数据对象,方便外部调用者获取数据进行其他相关操作
public NoteItemData getItemData() {
return mItemData;
}
}

@ -0,0 +1,519 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明,表明该类所属的包名,用于在项目中进行类的组织和管理
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
// NotesPreferenceActivity类继承自PreferenceActivity用于展示和管理应用的偏好设置相关界面及功能
public class NotesPreferenceActivity extends PreferenceActivity {
// 定义偏好设置的名称常量用于在获取和操作SharedPreferences时作为标识
public static final String PREFERENCE_NAME = "notes_preferences";
// 定义用于存储同步账户名称的偏好设置键常量
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
// 定义用于存储上次同步时间的偏好设置键常量
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
// 定义用于设置背景颜色相关功能的偏好设置键常量(可能是控制背景颜色随机显示等功能,具体需看业务逻辑)
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
// 定义与同步账户相关的偏好设置键常量,可能用于在偏好设置界面中定位或操作相关的账户设置项
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
// 定义用于过滤广播的权限相关键常量(具体用途需结合广播相关逻辑进一步确定)
private static final String AUTHORITIES_FILTER_KEY = "authorities";
// 用于在偏好设置界面中对账户相关的偏好设置项进行分类管理的PreferenceCategory对象
private PreferenceCategory mAccountCategory;
// 自定义的广播接收器对象,用于接收特定的广播消息(这里可能与同步服务相关的广播有关)
private GTaskReceiver mReceiver;
// 用于保存原始的账户列表信息,可能用于对比账户变化情况等操作
private Account[] mOriAccounts;
// 用于标记是否添加了新账户的布尔变量初始值为false
private boolean mHasAddedAccount;
// onCreate方法在Activity创建时被调用用于进行初始化相关的操作
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 设置ActionBar的显示属性使得应用图标可以用于导航通常点击图标可返回上一级界面
getActionBar().setDisplayHomeAsUpEnabled(true);
// 从指定的XML资源文件R.xml.preferences中加载偏好设置界面的布局和配置信息构建出偏好设置界面的初始显示内容
addPreferencesFromResource(R.xml.preferences);
// 通过键PREFERENCE_SYNC_ACCOUNT_KEY从已加载的偏好设置中找到对应的PreferenceCategory对象用于后续对账户相关设置项的操作
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// 创建一个GTaskReceiver实例用于接收相关广播消息
mReceiver = new GTaskReceiver();
// 创建一个IntentFilter对象用于指定要接收的广播动作这里只关注GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME对应的广播
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
// 注册广播接收器使得该Activity能够接收到符合过滤条件的广播消息
registerReceiver(mReceiver, filter);
// 初始化原始账户列表为null后续可能会获取并赋值实际的账户信息
mOriAccounts = null;
// 通过LayoutInflater从当前上下文获取布局Inflater并加载指定的布局资源R.layout.settings_header作为头部视图添加到当前Activity的列表视图中第三个参数设置为true表示该头部视图在列表滚动时固定显示
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
// onResume方法在Activity重新回到前台可见状态时被调用常用于恢复或更新界面相关的操作以及处理一些业务逻辑
@Override
protected void onResume() {
super.onResume();
// 如果已经添加了新账户mHasAddedAccount为true
if (mHasAddedAccount) {
// 获取当前的谷歌账户列表
Account[] accounts = getGoogleAccounts();
// 如果原始账户列表不为null且新获取的账户数量大于原始账户数量说明可能添加了新账户
if (mOriAccounts!= null && accounts.length > mOriAccounts.length) {
// 遍历新获取的账户列表
for (Account accountNew : accounts) {
// 标记是否找到匹配的原始账户初始化为false
boolean found = false;
// 遍历原始账户列表,对比账户名称是否相同
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
// 如果在原始账户列表中未找到匹配的账户,说明是新添加的账户
if (!found) {
// 设置同步账户为新添加的这个账户的名称,并跳出循环(只设置一个新账户作为同步账户即可,具体业务逻辑可能如此)
setSyncAccount(accountNew.name);
break;
}
}
}
}
// 调用refreshUI方法更新界面显示内容具体更新逻辑在该方法中实现代码中未完整展示该方法内容
refreshUI();
}
// 当Activity被销毁时调用的方法用于释放相关资源和进行一些清理操作
@Override
protected void onDestroy() {
// 如果广播接收器mReceiver不为空就注销该广播接收器避免内存泄漏等问题
if (mReceiver!= null) {
unregisterReceiver(mReceiver);
}
// 调用父类的onDestroy方法执行父类中定义的销毁相关的默认操作
super.onDestroy();
}
// 用于加载账户偏好设置相关内容的方法,例如在偏好设置界面中添加账户相关的设置项及设置其显示和点击等行为逻辑
private void loadAccountPreference() {
// 先移除账户偏好设置分类mAccountCategory下的所有已有偏好设置项确保后续添加的是最新的内容
mAccountCategory.removeAll();
// 创建一个新的Preference实例用于表示账户相关的偏好设置项
Preference accountPref = new Preference(this);
// 获取当前的默认同步账户名称通过调用getSyncAccountName方法
final String defaultAccount = getSyncAccountName(this);
// 设置该偏好设置项的标题,通过资源字符串获取对应的文本内容
accountPref.setTitle(getString(R.string.preferences_account_title));
// 设置该偏好设置项的摘要信息,通过资源字符串获取对应的文本内容
accountPref.setSummary(getString(R.string.preferences_account_summary));
// 为该偏好设置项设置点击监听器,当用户点击该项时触发相应的逻辑
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 如果同步服务GTaskSyncService当前没有正在进行同步操作
if (!GTaskSyncService.isSyncing()) {
// 如果默认账户名称为空字符串,说明是第一次设置账户
if (TextUtils.isEmpty(defaultAccount)) {
// 弹出选择账户的警告对话框用于让用户选择要设置的账户具体逻辑在showSelectAccountAlertDialog方法中
showSelectAccountAlertDialog();
} else {
// 如果已经设置过账户了弹出确认更改账户的警告对话框提示用户更改账户可能存在的风险具体逻辑在showChangeAccountConfirmAlertDialog方法中
showChangeAccountConfirmAlertDialog();
}
} else {
// 如果同步服务正在同步通过Toast提示用户当前不能更改账户
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
// 返回true表示已处理该点击事件
return true;
}
});
// 将创建并设置好的账户偏好设置项添加到账户偏好设置分类中
mAccountCategory.addPreference(accountPref);
}
// 用于加载同步按钮相关设置的方法,包括设置按钮的文本、点击事件以及同步状态相关的显示文本等内容
private void loadSyncButton() {
// 通过findViewById方法找到布局中的同步按钮Button实例
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
// 通过findViewById方法找到布局中的用于显示上次同步时间的TextView实例
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// 根据同步服务GTaskSyncService是否正在同步来设置同步按钮的文本和点击事件逻辑
// 如果正在同步
if (GTaskSyncService.isSyncing()) {
// 设置按钮文本为取消同步的文本内容(通过资源字符串获取)
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
// 为按钮设置点击监听器点击时调用GTaskSyncService的cancelSync方法来取消同步操作传入当前的NotesPreferenceActivity实例作为上下文
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
}
});
} else {
// 如果没有正在同步
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
// 为按钮设置点击监听器点击时调用GTaskSyncService的startSync方法来立即启动同步操作传入当前的NotesPreferenceActivity实例作为上下文
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this);
}
});
}
// 根据是否设置了同步账户来决定同步按钮是否可用,只有设置了同步账户(同步账户名称不为空字符串)时,按钮才可用
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// 根据同步服务的状态来设置上次同步时间显示文本及可见性
// 如果正在同步
if (GTaskSyncService.isSyncing()) {
// 设置显示文本为同步服务的进度字符串通过GTaskSyncService的getProgressString方法获取并将该TextView设置为可见
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 如果没有正在同步获取上次同步的时间戳通过调用getLastSyncTime方法
long lastSyncTime = getLastSyncTime(this);
// 如果上次同步时间戳不为0说明有上次同步记录
if (lastSyncTime!= 0) {
// 设置显示文本通过格式化字符串的方式将时间戳格式化为指定格式的日期时间字符串先通过资源字符串获取日期时间格式再进行格式化并将该TextView设置为可见
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 如果没有上次同步记录将该TextView设置为不可见
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
// 用于刷新整个偏好设置界面的方法,通过调用加载账户偏好设置和加载同步按钮相关的方法来更新界面显示内容
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
// 用于弹出选择账户的警告对话框的方法,在该对话框中展示可供选择的账户列表以及添加账户的入口等内容
private void showSelectAccountAlertDialog() {
// 创建一个AlertDialog.Builder实例用于构建警告对话框
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 通过LayoutInflater从当前上下文加载自定义的标题布局R.layout.account_dialog_title
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
// 从加载的标题布局中找到对应的标题TextView实例
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
// 设置标题TextView的文本内容通过资源字符串获取对应的文本
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
// 从加载的标题布局中找到对应的副标题TextView实例
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
// 设置副标题TextView的文本内容通过资源字符串获取对应的文本
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
// 将自定义的标题视图设置为对话框的标题部分
dialogBuilder.setCustomTitle(titleView);
// 设置对话框的确认按钮正按钮这里先设置为null后续可能会根据具体逻辑再设置或者在某些情况下不需要点击操作
dialogBuilder.setPositiveButton(null, null);
// 获取当前的谷歌账户列表通过调用getGoogleAccounts方法
Account[] accounts = getGoogleAccounts();
// 获取当前的默认同步账户名称通过调用getSyncAccountName方法
String defAccount = getSyncAccountName(this);
// 将获取的账户列表保存到mOriAccounts变量中用于后续可能的对比等操作
mOriAccounts = accounts;
// 标记是否添加了新账户初始化为false
mHasAddedAccount = false;
// 如果获取到的账户列表长度大于0说明有可用的账户可供选择
if (accounts.length > 0) {
// 创建一个字符序列数组,长度与账户列表长度相同,用于存储账户名称,作为单选列表项展示给用户
CharSequence[] items = new CharSequence[accounts.length];
// 创建一个临时的字符序列数组用于在点击单选列表项时进行账户名称的映射操作这里其实和items数组内容基本一致只是为了方便后续代码逻辑理解和编写
final CharSequence[] itemMapping = items;
// 用于记录默认选中的账户在列表中的索引位置,初始化为 -1表示没有默认选中项
int checkedItem = -1;
// 索引变量用于遍历账户列表并填充items数组以及查找默认选中项的索引
int index = 0;
// 遍历账户列表
for (Account account : accounts) {
// 如果当前账户名称与默认同步账户名称相等,说明该账户是当前默认选中的账户,记录其索引位置
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
// 将账户名称添加到items数组中作为单选列表项的显示内容
items[index++] = account.name;
}
// 设置对话框的单选列表项内容、默认选中项索引以及点击监听器当用户点击某个单选列表项时调用setSyncAccount方法设置选中的账户作为同步账户然后关闭对话框并刷新界面通过调用refreshUI方法
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
// 通过LayoutInflater从当前上下文加载添加账户相关的布局R.layout.add_account_text用于在对话框中展示添加账户的入口视图
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
// 将添加账户的入口视图添加到对话框中
dialogBuilder.setView(addAccountView);
// 显示构建好的警告对话框,并获取对话框实例
final AlertDialog dialog = dialogBuilder.show();
// 为添加账户的入口视图设置点击监听器当用户点击时标记已添加新账户mHasAddedAccount设置为true然后启动添加账户的系统设置页面通过意图指定相关动作和参数最后关闭当前对话框
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
// 用于弹出更改账户确认警告对话框的方法,在对话框中展示相关提示信息以及操作选项供用户选择
private void showChangeAccountConfirmAlertDialog() {
// 创建一个AlertDialog.Builder实例用于构建警告对话框
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 通过LayoutInflater从当前上下文加载自定义的标题布局R.layout.account_dialog_title
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
// 从加载的标题布局中找到对应的标题TextView实例
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
// 设置标题TextView的文本内容通过资源字符串获取对应的格式化文本其中格式化参数为当前的同步账户名称通过调用getSyncAccountName方法获取
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
// 从加载的标题布局中找到对应的副标题TextView实例
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
// 设置副标题TextView的文本内容通过资源字符串获取对应的警告提示文本
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
// 将自定义的标题视图设置为对话框的标题部分
dialogBuilder.setCustomTitle(titleView);
// 创建一个字符序列数组,用于存储对话框中展示的菜单项文本内容,包括更改账户、移除账户和取消三个选项
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
// 设置对话框的菜单项内容以及点击监听器,根据用户点击的菜单项索引执行不同的操作
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 如果用户点击的是“更改账户”菜单项索引为0
if (which == 0) {
// 弹出选择账户的警告对话框通过调用showSelectAccountAlertDialog方法让用户重新选择账户
showSelectAccountAlertDialog();
} else if (which == 1) {
// 如果用户点击的是“移除账户”菜单项索引为1则调用removeSyncAccount方法移除当前的同步账户并刷新界面通过调用refreshUI方法
removeSyncAccount();
refreshUI();
}
}
});
// 显示构建好的警告对话框
dialogBuilder.show();
}
// 用于获取当前设备上的谷歌账户列表的方法通过AccountManager来获取指定类型"com.google")的账户信息
private Account[] getGoogleAccounts() {
// 获取AccountManager实例传入当前上下文
AccountManager accountManager = AccountManager.get(this);
// 调用AccountManager的getAccountsByType方法获取类型为"com.google"的账户数组并返回
return accountManager.getAccountsByType("com.google");
}
// 用于设置同步账户的方法,根据传入的账户名称来更新偏好设置中的同步账户相关信息,并进行一些相关的数据清理和提示操作
private void setSyncAccount(String account) {
// 如果传入的账户与当前获取的同步账户名称不一致(说明要进行账户更改操作)
if (!getSyncAccountName(this).equals(account)) {
// 获取应用的SharedPreferences实例用于存储和读取偏好设置数据指定偏好设置名称PREFERENCE_NAME和私有访问模式Context.MODE_PRIVATE
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
// 获取SharedPreferences的编辑器用于修改偏好设置中的数据
SharedPreferences.Editor editor = settings.edit();
// 如果传入的账户不为null将其存入偏好设置中对应的键PREFERENCE_SYNC_ACCOUNT_NAME
if (account!= null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
// 如果传入的账户为null将同步账户名称设置为空字符串
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
// 提交对偏好设置的修改,使其生效
editor.commit();
// 调用setLastSyncTime方法将上次同步时间清理设置为0表示重新开始同步相关的计时等逻辑
setLastSyncTime(this, 0);
// 在一个新线程中执行清理本地与GTask相关信息的操作创建一个ContentValues实例用于存储要更新的数据
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
// 将笔记相关列NoteColumns.GTASK_ID的值设置为空字符串可能用于清除之前与该账户关联的同步任务相关标识
values.put(NoteColumns.GTASK_ID, "");
// 将同步相关列NoteColumns.SYNC_ID的值设置为0可能用于重置同步相关的状态标识
values.put(NoteColumns.SYNC_ID, 0);
// 通过内容解析器getContentResolver更新笔记相关的内容提供器Notes.CONTENT_NOTE_URI中的数据使用设置好的ContentValues进行更新这里更新条件为null可能是更新所有符合条件的数据具体需看内容提供器的实现逻辑
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
// 通过Toast提示用户账户设置成功显示的文本通过资源字符串获取并传入设置的账户名称作为格式化参数
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
// 用于移除同步账户的方法从偏好设置中移除同步账户相关的信息并清理本地与GTask相关的信息
private void removeSyncAccount() {
// 获取应用的SharedPreferences实例用于存储和读取偏好设置数据指定偏好设置名称PREFERENCE_NAME和私有访问模式Context.MODE_PRIVATE
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
// 获取SharedPreferences的编辑器用于修改偏好设置中的数据
SharedPreferences.Editor editor = settings.edit();
// 如果偏好设置中包含同步账户名称相关的键PREFERENCE_SYNC_ACCOUNT_NAME则移除该键对应的值
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
// 如果偏好设置中包含上次同步时间相关的键PREFERENCE_LAST_SYNC_TIME则移除该键对应的值
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
// 提交对偏好设置的修改,使其生效
editor.commit();
// 在一个新线程中执行清理本地与GTask相关信息的操作创建一个ContentValues实例用于存储要更新的数据
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
// 将笔记相关列NoteColumns.GTASK_ID的值设置为空字符串可能用于清除之前与该账户关联的同步任务相关标识
values.put(NoteColumns.GTASK_ID, "");
// 将同步相关列NoteColumns.SYNC_ID的值设置为0可能用于重置同步相关的状态标识
values.put(NoteColumns.SYNC_ID, 0);
// 通过内容解析器getContentResolver更新笔记相关的内容提供器Notes.CONTENT_NOTE_URI中的数据使用设置好的ContentValues进行更新这里更新条件为null可能是更新所有符合条件的数据具体需看内容提供器的实现逻辑
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
// 静态方法用于获取当前设置的同步账户名称从应用的SharedPreferences中读取对应键PREFERENCE_SYNC_ACCOUNT_NAME下存储的值如果不存在则返回空字符串
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
// 静态方法用于设置上次同步时间将指定的时间戳time存入应用的SharedPreferences中对应的键PREFERENCE_LAST_SYNC_TIME
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
}
// 静态方法用于获取上次同步时间从应用的SharedPreferences中读取对应键PREFERENCE_LAST_SYNC_TIME下存储的时间戳如果不存在则返回0
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
// 自定义的广播接收器类继承自BroadcastReceiver用于接收特定的广播消息并做出相应的响应
private class GTaskReceiver extends BroadcastReceiver {
// 当接收到广播消息时调用的方法,在这里进行界面刷新以及根据广播中的同步相关信息更新界面显示内容等操作
@Override
public void onReceive(Context context, Intent intent) {
// 调用refreshUI方法刷新整个偏好设置界面更新界面上的各种显示内容如账户信息、同步按钮状态等
refreshUI();
// 如果广播消息中携带的表示是否正在同步的额外数据通过GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING键获取为true说明正在同步
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
// 通过findViewById方法找到用于显示同步状态的TextView实例
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// 设置该TextView的文本内容为广播消息中携带的同步进度相关信息通过GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG键获取
syncStatus.setText(intent.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
// 用于处理选项菜单中菜单项被选中时的操作逻辑,例如处理返回按钮等菜单项的点击事件
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// 如果点击的是Home按钮通常是左上角的返回箭头图标用于返回上一级界面等操作
case android.R.id.home:
// 创建一个意图Intent指定要启动的目标Activity为NotesListActivity.class即返回笔记列表界面
Intent intent = new Intent(this, NotesListActivity.class);
// 添加标志位使得启动的Activity会清除其上的所有其他Activity实现返回栈的清理回到笔记列表界面并将其置于栈顶
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 根据意图启动Activity
startActivity(intent);
// 返回true表示已处理该菜单项点击事件
return true;
default:
// 如果点击的是其他未处理的菜单项返回false表示未处理该事件
return false;
}
}
}

@ -0,0 +1,178 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明,表明该类所属的包名,用于在项目中进行类的组织和管理
package net.micode.notes.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import android.widget.RemoteViews;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NoteEditActivity;
import net.micode.notes.ui.NotesListActivity;
// NoteWidgetProvider类是一个抽象类继承自AppWidgetProvider用于作为桌面小部件App Widget相关功能的基础类提供了一些通用的小部件操作逻辑以及定义了抽象方法供具体子类实现特定功能
public abstract class NoteWidgetProvider extends AppWidgetProvider {
// 定义一个字符串数组用于指定查询数据库时要获取的列信息这里包含笔记的ID、背景颜色ID以及摘要信息等列
public static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
};
// 定义常量用于表示在查询结果中笔记ID所在的列索引位置方便后续从游标Cursor中获取对应的数据
public static final int COLUMN_ID = 0;
// 定义常量用于表示在查询结果中背景颜色ID所在的列索引位置方便后续从游标Cursor中获取对应的数据
public static final int COLUMN_BG_COLOR_ID = 1;
// 定义常量用于表示在查询结果中摘要信息所在的列索引位置方便后续从游标Cursor中获取对应的数据
public static final int COLUMN_SNIPPET = 2;
// 定义用于日志记录的标签,方便在日志输出中识别该类相关的日志信息
private static final String TAG = "NoteWidgetProvider";
// 当桌面小部件被删除时调用的方法,用于处理相关的数据清理等操作
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 创建一个ContentValues实例用于存储要更新到数据库中的数据
ContentValues values = new ContentValues();
// 将笔记相关列NoteColumns.WIDGET_ID的值设置为无效的小部件IDAppWidgetManager.INVALID_APPWIDGET_ID表示该笔记不再关联被删除的小部件
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
// 遍历被删除的小部件ID数组
for (int i = 0; i < appWidgetIds.length; i++) {
// 通过内容解析器context.getContentResolver()更新笔记相关的内容提供器Notes.CONTENT_NOTE_URI中的数据使用设置好的ContentValues进行更新更新条件为笔记的小部件ID列等于当前遍历到的被删除小部件的ID
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?",
new String[] { String.valueOf(appWidgetIds[i])});
}
}
// 私有方法用于获取与指定小部件ID相关的笔记小部件信息通过查询数据库来获取相应的数据
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
// 使用内容解析器发起查询操作查询的内容提供器为Notes.CONTENT_NOTE_URI指定要获取的列信息为之前定义的PROJECTION数组中的列查询条件为笔记的小部件ID等于传入的小部件ID且父ID不等于回收站文件夹的IDNotes.ID_TRASH_FOLER查询条件的参数通过字符串数组传入最后一个参数null表示不指定排序方式等额外条件
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },
null);
}
// 受保护的方法用于更新桌面小部件的显示内容等信息调用了另一个重载的update方法并传入默认参数false
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
// 私有方法用于实际更新桌面小部件的显示内容等信息根据传入的小部件ID数组对每个小部件进行相应的更新操作
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
// 遍历传入的小部件ID数组
for (int i = 0; i < appWidgetIds.length; i++) {
// 如果小部件ID不是无效的小部件IDAppWidgetManager.INVALID_APPWIDGET_ID才进行更新操作
if (appWidgetIds[i]!= AppWidgetManager.INVALID_APPWIDGET_ID) {
// 获取默认的背景颜色ID通过ResourceParser工具类的方法获取具体获取逻辑在该工具类中实现
int bgId = ResourceParser.getDefaultBgId(context);
// 初始化摘要信息为空字符串,后续会根据实际查询结果进行赋值
String snippet = "";
// 创建一个意图Intent指定要启动的目标Activity为NoteEditActivity.class即点击小部件后可能会跳转到的编辑笔记的Activity
Intent intent = new Intent(context, NoteEditActivity.class);
// 设置意图的标志位使得如果该Activity已经在栈顶则不会重新创建实例而是复用已有的实例例如避免重复打开同一个编辑页面等情况
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
// 将小部件的ID作为额外数据添加到意图中方便在目标Activity中获取并使用通过Notes.INTENT_EXTRA_WIDGET_ID键进行传递
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
// 将小部件的类型作为额外数据添加到意图中具体的小部件类型由子类实现的getWidgetType方法获取并传入通过Notes.INTENT_EXTRA_WIDGET_TYPE键进行传递
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
// 获取与当前小部件ID相关的笔记小部件信息的游标Cursor通过调用getNoteWidgetInfo方法查询数据库获取
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
// 如果游标不为空且游标可以移动到第一条数据(表示查询到了相关数据)
if (c!= null && c.moveToFirst()) {
// 如果查询结果的数量大于1说明出现了同一个小部件ID关联多条消息的异常情况记录错误日志并关闭游标然后直接返回不进行后续更新操作
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return;
}
// 获取摘要信息列的值赋值给snippet变量用于后续在小部件上显示
snippet = c.getString(COLUMN_SNIPPET);
// 获取背景颜色ID列的值赋值给bgId变量用于设置小部件的背景相关显示
bgId = c.getInt(COLUMN_BG_COLOR_ID);
// 将笔记的ID作为额外数据添加到意图中通过Intent.EXTRA_UID键传递方便在目标Activity中根据ID进行相应操作
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID));
// 设置意图的动作Action为查看Intent.ACTION_VIEW表示点击小部件可能执行查看笔记的操作具体行为由目标Activity根据该动作进一步处理
intent.setAction(Intent.ACTION_VIEW);
} else {
// 如果游标为空或者没有查询到相关数据,设置摘要信息为资源字符串中对应的提示文本(表示小部件没有关联内容)
snippet = context.getResources().getString(R.string.widget_havenot_content);
// 设置意图的动作Action为插入或编辑Intent.ACTION_INSERT_OR_EDIT表示点击小部件可能执行新建或编辑笔记的操作具体行为由目标Activity根据该动作进一步处理
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
// 如果游标不为空,关闭游标,释放相关资源
if (c!= null) {
c.close();
}
// 创建一个RemoteViews实例用于构建桌面小部件的远程视图传入当前上下文的包名和具体的布局ID由子类实现的getLayoutId方法获取
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
// 设置小部件中背景图片资源的ID通过调用抽象方法getBgResourceId传入背景颜色ID来获取对应的资源ID进行设置
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
// 将背景颜色ID作为额外数据添加到意图中通过Notes.INTENT_EXTRA_BACKGROUND_ID键传递方便在目标Activity中根据背景颜色进行相应操作可能涉及界面显示等方面的适配
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
// 根据是否处于隐私模式privacyMode来生成不同的PendingIntent实例用于设置小部件点击事件的响应逻辑
PendingIntent pendingIntent = null;
if (privacyMode) {
// 如果处于隐私模式,设置小部件上文本显示为隐私模式相关的提示文本(通过资源字符串获取)
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
// 创建一个PendingIntent用于启动NotesListActivity可能是进入某种隐私模式下的列表页面等操作传入当前上下文、小部件ID以及要启动的意图等参数并设置标志位为PendingIntent.FLAG_UPDATE_CURRENT表示如果已存在相同的PendingIntent则更新其额外数据等内容
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
// 如果不处于隐私模式设置小部件上文本显示为之前获取或设置的摘要信息snippet
rv.setTextViewText(R.id.widget_text, snippet);
// 创建一个PendingIntent用于启动之前设置的意图intent可能是跳转到编辑笔记等相关Activity传入当前上下文、小部件ID以及要启动的意图等参数并设置标志位为PendingIntent.FLAG_UPDATE_CURRENT表示如果已存在相同的PendingIntent则更新其额外数据等内容
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
// 设置小部件中文本视图R.id.widget_text的点击事件响应PendingIntent即点击小部件上的文本区域时会触发相应的意图操作
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
// 通过AppWidgetManager更新指定小部件ID对应的小部件的远程视图实现小部件显示内容的更新
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
}
}
// 抽象方法用于获取与指定背景颜色ID对应的背景资源ID由具体的子类根据实际的资源映射关系来实现该方法以设置小部件的背景图片等显示相关资源
protected abstract int getBgResourceId(int bgId);
// 抽象方法用于获取小部件对应的布局资源ID由具体的子类根据不同类型小部件的布局需求来实现该方法以确定小部件的整体布局样式
protected abstract int getLayoutId();
// 抽象方法,用于获取小部件的类型,由具体的子类根据实际小部件的类型定义来实现该方法,以便在更新等操作中传递准确的小部件类型信息
protected abstract int getWidgetType();
}

@ -0,0 +1,53 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明,表明该类所属的包名,用于在项目中进行类的组织和管理
package net.micode.notes.widget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
// NoteWidgetProvider_2x类继承自NoteWidgetProvider抽象类是针对特定尺寸可能是 2x 尺寸,具体需结合应用场景理解)桌面小部件的具体实现类,用于实现该尺寸小部件的特定功能,比如布局、背景资源以及类型等相关设置。
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
// 重写父类的onUpdate方法该方法在桌面小部件需要更新时被调用例如小部件添加到桌面、系统触发小部件更新等情况
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 调用父类的update方法来执行小部件的更新操作将当前上下文、AppWidgetManager实例以及小部件ID数组传递给父类方法由父类中定义的通用更新逻辑来处理更新相关事宜例如设置小部件的显示内容、点击事件等具体逻辑在父类的update方法中
super.update(context, appWidgetManager, appWidgetIds);
}
// 重写父类的抽象方法getLayoutId用于返回该尺寸小部件对应的布局资源ID这里返回的是R.layout.widget_2x表示使用名为widget_2x的布局文件来展示该小部件的外观样式
@Override
protected int getLayoutId() {
return R.layout.widget_2x;
}
// 重写父类的抽象方法getBgResourceId用于根据传入的背景颜色ID获取对应的背景资源ID通过调用ResourceParser.WidgetBgResources工具类的相关方法getWidget2xBgResource来获取适合该尺寸小部件的背景资源ID实现了根据不同背景颜色ID来设置小部件背景图片等显示资源的功能
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId);
}
// 重写父类的抽象方法getWidgetType用于返回该小部件的类型这里返回的是Notes.TYPE_WIDGET_2X表示该小部件属于特定的2x类型具体类型含义可能由应用中对不同尺寸或功能小部件的分类定义决定以便在小部件相关的操作和逻辑中能准确区分不同类型的小部件
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X;
}
}

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 包声明,指定该类所在的包名,便于在项目中对类进行组织管理以及进行包相关的访问控制等操作
package net.micode.notes.widget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
// NoteWidgetProvider_4x类继承自NoteWidgetProvider抽象类是专门针对某种特定的4x尺寸可能是桌面小部件尺寸规格具体取决于应用定义桌面小部件功能实现的类通过重写抽象类中的相关方法来定制该尺寸小部件的具体行为和外观展示等内容。
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
// 重写父类的onUpdate方法该方法会在桌面小部件需要更新时被调用例如小部件首次添加到桌面、系统定时触发小部件更新或者应用内有相关更新操作触发等情况。
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 调用父类的update方法来执行具体的更新操作将当前上下文、AppWidgetManager实例以及小部件ID数组作为参数传递过去由父类中通用的更新逻辑来处理诸如设置小部件显示内容、配置点击事件等更新相关的操作父类update方法中已定义了这些通用逻辑
super.update(context, appWidgetManager, appWidgetIds);
}
// 重写父类的抽象方法getLayoutId用于返回该4x尺寸小部件对应的布局资源ID这里返回R.layout.widget_4x表示该小部件将会使用名为widget_4x的布局文件来构建其界面显示样式该布局文件中定义了小部件各个元素的布局结构、样式等信息。
protected int getLayoutId() {
return R.layout.widget_4x;
}
// 重写父类的抽象方法getBgResourceId用于根据传入的背景颜色ID获取对应的背景资源ID通过调用ResourceParser.WidgetBgResources工具类中的getWidget4xBgResource方法来获取适合该4x尺寸小部件的背景资源ID以此实现根据不同的背景颜色需求来准确设置小部件的背景图片等显示相关资源使得小部件的外观能根据不同情况进行相应变化。
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId);
}
// 重写父类的抽象方法getWidgetType用于返回该小部件的类型标识这里返回Notes.TYPE_WIDGET_4X表示该小部件属于特定的4x类型在应用中可能依据不同尺寸、功能等对小部件进行了分类定义通过这个类型标识来区分方便在整个桌面小部件相关的逻辑处理中如更新、交互等操作准确识别该小部件的类型并进行针对性的处理。
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X;
}
}
Loading…
Cancel
Save