注释代码

chiyunfei
ccyf 9 months ago
parent 19443e83ed
commit 010605e9ab

@ -1,19 +1,3 @@
/*
* 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;
@ -21,92 +5,130 @@ 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;
// DateTimePicker类继承自FrameLayout用于创建一个可选择日期和时间的自定义视图组件
public class DateTimePicker extends FrameLayout {
// 默认的启用状态设置为true表示默认启用该组件
private static final boolean DEFAULT_ENABLE_STATE = true;
// 半天包含的小时数用于12小时制相关的计算和判断值为12
private static final int HOURS_IN_HALF_DAY = 12;
// 一天包含的小时数用于24小时制相关的操作值为24
private static final int HOURS_IN_ALL_DAY = 24;
// 一周包含的天数用于日期选择器取值范围等相关操作值为7
private static final int DAYS_IN_ALL_WEEK = 7;
// 日期选择器的最小取值通常从0开始对应一周中的第一天
private static final int DATE_SPINNER_MIN_VAL = 0;
// 日期选择器的最大取值对应一周中的最后一天一周共7天索引从0开始所以最大值为6
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
// 24小时制视图下小时选择器的最小取值为0
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
// 24小时制视图下小时选择器的最大取值为23
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
// 12小时制视图下小时选择器的最小取值为112小时制中通常显示1 - 12
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
// 12小时制视图下小时选择器的最大取值为12
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
// 分钟选择器的最小取值为0
private static final int MINUT_SPINNER_MIN_VAL = 0;
// 分钟选择器的最大取值为59一分钟包含60秒取值范围0 - 59
private static final int MINUT_SPINNER_MAX_VAL = 59;
// 上午/下午选择器的最小取值通常为0对应上午等相关索引
private static final int AMPM_SPINNER_MIN_VAL = 0;
// 上午/下午选择器的最大取值通常为1对应下午等相关索引
private static final int AMPM_SPINNER_MAX_VAL = 1;
// 对应日期的NumberPicker用于选择具体的某一天周内
private final NumberPicker mDateSpinner;
// 对应小时的NumberPicker用于选择具体的小时数
private final NumberPicker mHourSpinner;
// 对应分钟的NumberPicker用于选择具体的分钟数
private final NumberPicker mMinuteSpinner;
// 对应上午/下午的NumberPicker用于12小时制下选择上午或下午
private final NumberPicker mAmPmSpinner;
// 用于存储当前所表示的日期和时间的Calendar实例
private Calendar mDate;
// 用于存储一周内各天显示名称的数组长度为一周的天数7天
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
// 表示当前是否为上午用于12小时制的判断和显示控制
private boolean mIsAm;
// 表示是否采用24小时制视图true为24小时制false为12小时制
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) {
// 根据新值和旧值的差值调整Calendar中的日期在周内切换日期
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();
// 如果不是24小时制视图即12小时制
if (!mIs24HourView) {
// 如果当前不是上午且旧值为1112小时制中半天的最后一小时新值变为12切换到下午日期可能需要变更
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) {
}
// 如果当前是上午且旧值为12新值变为11切换到上午日期可能需要变更
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;
}
// 如果涉及上午/下午切换11点到12点或者12点到11点
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;
mIsAm =!mIsAm;
// 更新上午/下午显示相关的控件状态
updateAmPmControl();
}
} else {
// 如果是24小时制视图当旧值为23一天的最后一小时新值变为0切换到新的一天
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) {
}
// 当旧值为0新值变为23回退到前一天
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);
// 计算并设置新的小时数考虑12小时制转换等情况
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));
@ -115,22 +137,28 @@ public class DateTimePicker extends FrameLayout {
}
};
// 分钟选择器的值改变监听器,当分钟选择器的值改变时触发
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) {
}
// 如果旧值为最小值,新值变为最大值(可能意味着时间跨了一个小时边界,向后滚动了)
else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
if (offset != 0) {
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();
@ -139,58 +167,78 @@ public class DateTimePicker extends FrameLayout {
updateAmPmControl();
}
}
// 设置新的分钟数到Calendar实例中
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;
mIsAm =!mIsAm;
// 如果切换到上午时间往前推半天12小时
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
}
// 如果切换到下午时间往后推半天12小时
else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
// 更新上午/下午显示相关的控件状态
updateAmPmControl();
// 触发日期时间改变的回调方法,通知外部监听器上午/下午改变了
onDateTimeChanged();
}
};
// 定义接口用于外部类监听日期时间改变事件外部类需要实现该接口的onDateTimeChanged方法
public interface OnDateTimeChangedListener {
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
// 构造函数使用系统当前时间创建DateTimePicker实例
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
// 构造函数根据传入的时间戳date创建DateTimePicker实例采用系统当前设置的时间格式24小时制或12小时制
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
// 构造函数根据传入的时间戳date和指定的时间格式is24HourView参数决定创建DateTimePicker实例
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
// 获取当前时间的Calendar实例用于初始化mDate
mDate = Calendar.getInstance();
mInitialising = true;
// 根据当前小时数判断是否为上午用于初始化mIsAm
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);
// 获取上午/下午选择器显示的字符串数组(例如["AM", "PM"]
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
@ -198,22 +246,25 @@ public class DateTimePicker extends FrameLayout {
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// update controls to initial state
// 更新各控件到初始状态,如日期、小时、上午/下午显示等
updateDateControl();
updateHourControl();
updateAmPmControl();
// 设置是否为24小时制视图
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) {
@ -227,25 +278,18 @@ public class DateTimePicker extends FrameLayout {
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);
@ -253,15 +297,7 @@ public class DateTimePicker extends FrameLayout {
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);
@ -271,20 +307,12 @@ public class DateTimePicker extends FrameLayout {
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;
@ -294,192 +322,6 @@ public class DateTimePicker extends FrameLayout {
onDateTimeChanged();
}
/**
* Get current month in the year
*
* @return The current month in the year
*/
// 获取当前的月份0 - 11表示1 - 12月
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());
}
}
}
return mDate.get(Calendar.MONTH);

@ -1,19 +1,3 @@
/*
* 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.util.Calendar;
@ -29,60 +13,93 @@ import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
// DateTimePickerDialog类继承自AlertDialog用于展示一个包含日期时间选择器的对话框并处理相关的交互逻辑
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 用于存储当前所表示的日期和时间的Calendar实例初始化为系统当前时间
private Calendar mDate = Calendar.getInstance();
// 表示是否采用24小时制视图true为24小时制false为12小时制
private boolean mIs24HourView;
// 定义一个接口类型的变量,用于监听用户设置日期时间后的操作,外部类实现该接口可获取相关回调
private OnDateTimeSetListener mOnDateTimeSetListener;
// 包含的DateTimePicker实例用于在对话框中实际展示日期时间选择的界面
private DateTimePicker mDateTimePicker;
// 定义接口用于外部类监听用户在对话框中设置好日期时间后的事件外部类需要实现该接口的OnDateTimeSet方法
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date);
}
// 构造函数根据传入的时间戳date创建DateTimePickerDialog实例
public DateTimePickerDialog(Context context, long date) {
super(context);
// 创建一个DateTimePicker实例用于在对话框中选择日期和时间
mDateTimePicker = new DateTimePicker(context);
// 将DateTimePicker设置为对话框的视图内容使其在对话框中显示出来
setView(mDateTimePicker);
// 为DateTimePicker设置日期时间改变的监听器当用户在选择器中改变日期时间时触发
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 根据选择器中改变后的年、月、日、时、分更新mDate中的对应值保持内部存储的日期时间与选择器一致
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());
}
});
// 根据传入的时间戳设置mDate的时间并将秒数设置为0通常选择日期时间时秒数不需要精确设置
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0);
// 设置DateTimePicker显示的初始日期时间为mDate所表示的时间
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 设置对话框的确定按钮文本并将当前类实现了OnClickListener接口作为点击监听器
setButton(context.getString(R.string.datetime_dialog_ok), this);
// 设置对话框的取消按钮文本点击监听器设置为null表示默认的取消行为关闭对话框
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 根据系统当前设置的时间格式24小时制或12小时制设置对话框是否采用24小时制视图
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 初始更新对话框标题,展示初始的日期时间
updateTitle(mDate.getTimeInMillis());
}
// 设置对话框是否采用24小时制视图同时更新内部的标记变量mIs24HourView
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
// 设置用户设置日期时间后的监听器外部类通过实现OnDateTimeSetListener接口来处理后续逻辑
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
// 根据传入的时间戳date更新对话框标题显示的文本内容按照指定的格式展示日期时间
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_24HOUR;
DateUtils.FORMAT_SHOW_YEAR | // 显示年份
DateUtils.FORMAT_SHOW_DATE | // 显示日期(月、日等)
DateUtils.FORMAT_SHOW_TIME; // 显示时间(时、分等)
// 根据是否为24小时制视图添加对应的时间格式标记用于控制时间的显示格式24小时制或12小时制带上午/下午)
flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR;
// 使用DateUtils工具类按照设定的格式flag格式化日期时间并设置为对话框的标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
// 实现OnClickListener接口的方法当对话框的确定按钮被点击时触发
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener != null) {
// 如果设置了OnDateTimeSetListener监听器调用其OnDateTimeSet方法将对话框实例和当前选择的日期时间以时间戳形式传递出去
if (mOnDateTimeSetListener!= null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}

@ -1,19 +1,3 @@
/*
* 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;
@ -27,17 +11,32 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import net.micode.notes.R;
// DropdownMenu类用于创建一个带有弹出式菜单的按钮组件点击按钮可弹出菜单方便用户进行相关操作选择
public class DropdownMenu {
// 关联的按钮,点击该按钮将弹出菜单,用于展示和触发弹出菜单的显示操作
private Button mButton;
// 弹出式菜单实例,承载具体的菜单项,提供弹出显示及相关交互功能
private PopupMenu mPopupMenu;
// 对应弹出式菜单中的菜单对象,用于操作菜单项,如查找、设置属性等
private Menu mMenu;
// 构造函数用于初始化DropdownMenu实例传入上下文、关联的按钮以及菜单资源ID
public DropdownMenu(Context context, Button button, int menuId) {
mButton = button;
// 设置按钮的背景资源为指定的下拉图标,用于给用户视觉提示该按钮可弹出菜单
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 创建与给定按钮关联的PopupMenu实例使其弹出位置基于该按钮
mPopupMenu = new PopupMenu(context, mButton);
// 获取PopupMenu中的Menu对象后续用于操作菜单项
mMenu = mPopupMenu.getMenu();
// 使用菜单填充器根据传入的菜单资源ID通常来自资源文件将菜单项填充到Menu对象中
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 为按钮设置点击监听器,当按钮被点击时,显示弹出式菜单
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mPopupMenu.show();
@ -45,17 +44,20 @@ public class DropdownMenu {
});
}
// 设置弹出式菜单中菜单项的点击监听器外部类实现OnMenuItemClickListener接口来处理菜单项点击后的逻辑
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
if (mPopupMenu!= null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
// 根据传入的菜单项ID在Menu对象中查找对应的菜单项并返回方便后续对特定菜单项进行操作
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
// 设置按钮上显示的文本内容,用于改变按钮展示给用户的标题信息
public void setTitle(CharSequence title) {
mButton.setText(title);
}
}
}

@ -1,19 +1,3 @@
/*
* 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;
@ -28,53 +12,73 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
// FoldersListAdapter类继承自CursorAdapter用于将数据库游标中的数据适配显示到特定的视图列表中这里主要是针对文件夹相关信息的展示
public class FoldersListAdapter extends CursorAdapter {
// 定义一个字符串数组用于指定从数据库查询时需要获取的列名这里包含了文件夹的ID和摘要信息
public static final String [] PROJECTION = {
NoteColumns.ID,
NoteColumns.SNIPPET
};
// 定义常量表示在游标结果中文件夹ID所在列的索引方便后续从游标中获取对应数据初始化为0
public static final int ID_COLUMN = 0;
// 定义常量表示在游标结果中文件夹名称所在列的索引此处用摘要信息来代表名称相关内容方便后续从游标中获取对应数据初始化为1
public static final int NAME_COLUMN = 1;
// 构造函数调用父类CursorAdapter的构造函数来进行基础的初始化传入上下文和游标对象
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO Auto-generated constructor stub
// TODO Auto-generated constructor stub,此处可根据实际需求添加更多初始化逻辑,如果有的话
}
// 创建新的视图列表项视图用于显示游标中对应的数据这里返回一个自定义的FolderListItem视图实例
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
// 将游标中的数据绑定到给定的视图上,实现数据在视图中的显示填充
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 判断传入的视图是否是FolderListItem类型如果是则进行数据绑定操作
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);
String folderName = null;
// 如果游标中获取的文件夹ID等于特定的根文件夹IDNotes.ID_ROOT_FOLDER可能是预定义的根文件夹标识
if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) {
// 则将文件夹名称设置为对应的字符串资源(可能是显示“上级文件夹”之类的文本)
folderName = context.getString(R.string.menu_move_parent_folder);
} else {
// 否则从游标中获取代表文件夹名称的字符串通过NAME_COLUMN索引获取
folderName = cursor.getString(NAME_COLUMN);
}
// 调用FolderListItem的bind方法将获取到的文件夹名称设置到对应的TextView中进行显示
((FolderListItem) view).bind(folderName);
}
}
// 根据给定的位置,获取对应位置的文件夹名称,通过先获取对应位置的游标数据,再按照规则提取名称
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);
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
// 内部私有类继承自LinearLayout代表列表中的单个文件夹列表项视图用于承载和显示文件夹相关信息
private class FolderListItem extends LinearLayout {
// 用于显示文件夹名称的TextView控件
private TextView mName;
// 构造函数调用父类LinearLayout的构造函数进行初始化并加载对应的布局文件获取其中的TextView实例
public FolderListItem(Context context) {
super(context);
inflate(context, R.layout.folder_list_item, this);
mName = (TextView) findViewById(R.id.tv_folder_name);
}
// 用于将给定的文件夹名称设置到内部的TextView中进行显示的方法
public void bind(String name) {
mName.setText(name);
}
}
}
}

@ -1,19 +1,3 @@
/*
* 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.app.Activity;
@ -71,19 +55,19 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// NoteEditActivity类继承自Activity是一个用于编辑笔记的Activity实现了多个接口来处理各种用户交互和笔记相关的操作逻辑
public class NoteEditActivity extends Activity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
private class HeadViewHolder {
public TextView tvModified;
public ImageView ivAlertIcon;
public TextView tvAlertDate;
public ImageView ibSetBgColor;
// 内部类HeadViewHolder用于方便地持有和管理笔记头部视图中的各个子控件
private class HeadViewHolder {
public TextView tvModified; // 用于显示笔记修改时间的TextView控件
public ImageView ivAlertIcon; // 用于显示提醒相关图标的ImageView控件比如闹钟图标表示有提醒设置
public TextView tvAlertDate; // 用于展示提醒时间具体信息的TextView控件
public ImageView ibSetBgColor; // 用于触发设置笔记背景颜色操作的ImageView按钮
}
// 一个静态的Map用于将背景颜色选择按钮的ID映射到对应的颜色资源ID方便根据按钮操作获取相应的颜色设置
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
static {
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
@ -93,6 +77,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE);
}
// 一个静态的Map用于将颜色资源ID映射到对应的背景颜色选择按钮选中状态显示按钮的ID用于在界面上展示当前选中的颜色
private static final Map<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select);
@ -102,6 +87,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select);
}
// 一个静态的Map用于将字体大小选择按钮所在布局的ID映射到对应的字体大小资源ID方便根据按钮操作获取相应的字体大小设置
private static final Map<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();
static {
sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE);
@ -110,6 +96,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER);
}
// 一个静态的Map用于将字体大小资源ID映射到对应的字体选择按钮选中状态显示按钮的ID用于在界面上展示当前选中的字体大小
private static final Map<Integer, Integer> sFontSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select);
@ -118,59 +105,74 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select);
}
// 用于在日志中标识该类相关操作的TAG字符串方便调试和查看日志时区分来源
private static final String TAG = "NoteEditActivity";
// 笔记头部视图相关控件的持有者实例,用于后续操作头部视图中的各个控件
private HeadViewHolder mNoteHeaderHolder;
// 笔记头部视图的整体布局视图,可设置其背景等属性来展示笔记的相关信息
private View mHeadViewPanel;
// 用于展示背景颜色选择器的视图,包含多个可供选择背景颜色的按钮等交互元素
private View mNoteBgColorSelector;
// 用于展示字体大小选择器的视图,包含不同字体大小选项的按钮等交互元素
private View mFontSizeSelector;
// 用于编辑笔记内容的EditText控件用户在此输入和修改笔记的文本内容
private EditText mNoteEditor;
// 包含笔记编辑区域(可能有滚动等相关功能)的整体布局视图,可设置其背景等属性
private View mNoteEditorPanel;
// 代表当前正在编辑的笔记的WorkingNote实例封装了笔记的各种属性、数据以及相关操作方法
private WorkingNote mWorkingNote;
// 用于获取和操作应用的共享偏好设置,可保存和读取如字体大小等用户个性化配置信息
private SharedPreferences mSharedPrefs;
// 当前选择的字体大小资源ID通过共享偏好设置获取或设置用于控制笔记编辑文本的显示字体大小
private int mFontSizeId;
// 定义在共享偏好设置中用于存储字体大小偏好的键名,通过该键名来存取字体大小相关的配置信息
private static final String PREFERENCE_FONT_SIZE = "pref_font_size";
// 定义快捷方式图标标题的最大长度限制,用于生成发送到桌面的快捷方式图标显示的标题时,对过长的笔记内容进行截取
private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10;
// 表示列表项已勾选的标记字符串,通常用于在笔记的列表模式下展示勾选状态
public static final String TAG_CHECKED = String.valueOf('\u221A');
// 表示列表项未勾选的标记字符串,同样用于笔记列表模式下展示相应状态
public static final String TAG_UNCHECKED = String.valueOf('\u25A1');
// 用于存放多行编辑文本如果笔记处于列表模式下包含多个编辑项的布局容器的LinearLayout方便对多行文本进行管理和展示
private LinearLayout mEditTextList;
// 用户搜索查询的文本内容,用于在编辑笔记时实现如高亮显示搜索结果等与搜索相关的功能
private String mUserQuery;
// 用于进行文本匹配的正则表达式模式对象,根据用户查询内容在笔记文本中进行匹配查找等操作
private Pattern mPattern;
// Activity创建时调用的方法进行初始化操作如设置布局、根据Intent初始化Activity状态等
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置该Activity对应的布局文件这里加载的是笔记编辑界面的布局
this.setContentView(R.layout.note_edit);
if (savedInstanceState == null && !initActivityState(getIntent())) {
// 如果是初次创建savedInstanceState为null且无法成功初始化Activity状态则结束该Activity
if (savedInstanceState == null &&!initActivityState(getIntent())) {
finish();
return;
}
// 初始化相关资源,比如查找界面中的各个视图控件等操作
initResources();
}
/**
* Current activity may be killed when the memory is low. Once it is killed, for another time
* user load this activity, we should restore the former state
* Activity
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
// 如果保存的实例状态不为null且包含特定的额外信息如Intent的UID
if (savedInstanceState!= null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
// 尝试重新初始化Activity状态如果失败则结束该Activity
if (!initActivityState(intent)) {
finish();
return;
@ -179,24 +181,27 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
/**
* IntentActivityIntent
*
*
* @param intent ActivityIntent
* @return Activitytruefalse
*/
private boolean initActivityState(Intent intent) {
/**
* If the user specified the {@link Intent#ACTION_VIEW} but not provided with id,
* then jump to the NotesListActivity
*/
mWorkingNote = null;
// 如果Intent的动作是查看笔记ACTION_VIEW
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
mUserQuery = "";
/**
* Starting from the searched result
*/
// 如果Intent中包含搜索相关的额外数据表示从搜索结果进入编辑页面
if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY);
}
// 检查指定ID的笔记是否在笔记数据库中可见如果不可见则跳转到笔记列表页面提示用户笔记不存在然后结束当前Activity
if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) {
Intent jump = new Intent(this, NotesListActivity.class);
startActivity(jump);
@ -204,6 +209,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
} else {
// 加载指定ID的笔记数据到WorkingNote实例中如果加载失败则结束当前Activity
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load note failed with note id" + noteId);
@ -211,11 +217,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return false;
}
}
// 设置软键盘的显示模式初始时隐藏软键盘并且当软键盘弹出时调整Activity布局以适应键盘显示
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
} else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// New note
} else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// 如果Intent动作是新建或编辑笔记ACTION_INSERT_OR_EDIT
long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0);
int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
@ -224,14 +231,15 @@ public class NoteEditActivity extends Activity implements OnClickListener,
int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID,
ResourceParser.getDefaultBgId(this));
// Parse call-record note
// 解析通话记录笔记相关信息(如果有)
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0);
if (callDate != 0 && phoneNumber != null) {
if (callDate!= 0 && phoneNumber!= null) {
if (TextUtils.isEmpty(phoneNumber)) {
Log.w(TAG, "The call record number is null");
}
long noteId = 0;
// 根据电话号码和通话日期查找对应的笔记ID如果找到则加载该笔记否则创建一个新的空笔记并转换为通话记录笔记格式
if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
phoneNumber, callDate)) > 0) {
mWorkingNote = WorkingNote.load(this, noteId);
@ -250,6 +258,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
bgResId);
}
// 设置软键盘的显示模式初始时软键盘可见并且当软键盘弹出时调整Activity布局以适应键盘显示
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@ -258,616 +267,55 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
}
// 设置WorkingNote的设置状态变更监听器为当前Activity实现了NoteSettingChangedListener接口以便接收笔记相关设置变更的通知
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
// 当Activity恢复到前台时调用的方法用于重新初始化笔记编辑界面的显示内容等相关操作
@Override
protected void onResume() {
super.onResume();
initNoteScreen();
}
/**
*
* 使
*/
private void initNoteScreen() {
// 根据当前选择的字体大小资源ID设置笔记编辑文本的外观样式主要是字体大小
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
.getTexAppearanceResource(mFontSizeId));
// 如果笔记处于列表模式(有勾选列表项等功能)
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(mWorkingNote.getContent());
} else {
// 否则设置笔记编辑文本内容为包含高亮显示用户查询结果的文本,并将光标定位到文本末尾
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
// 隐藏所有背景颜色选择的选中状态显示按钮
for (Integer id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
}
// 设置笔记头部视图面板的背景资源为笔记对应的标题背景资源ID以展示相应的视觉效果
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
// 设置笔记编辑面板的背景资源为笔记对应的背景颜色资源ID改变编辑区域的背景显示
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
// 设置笔记头部的修改时间显示文本,按照指定的日期时间格式展示笔记的最后修改时间
mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR));
/**
* TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker
* is not ready
*/
// 显示提醒相关的头部信息(如提醒图标和提醒时间文本等,根据是否设置了提醒来决定显示与否)
showAlertHeader();
}
private void showAlertHeader() {
if (mWorkingNote.hasClockAlert()) {
long time = System.currentTimeMillis();
if (time > mWorkingNote.getAlertDate()) {
mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired);
} else {
mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString(
mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS));
}
mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE);
mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE);
} else {
mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE);
mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE);
};
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
initActivityState(intent);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
/**
* For new note without note id, we should firstly save it to
* generate a id. If the editing note is not worth saving, there
* is no id which is equivalent to create new note
*/
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId());
Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState");
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mNoteBgColorSelector, ev)) {
mNoteBgColorSelector.setVisibility(View.GONE);
return true;
}
if (mFontSizeSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mFontSizeSelector, ev)) {
mFontSizeSelector.setVisibility(View.GONE);
return true;
}
return super.dispatchTouchEvent(ev);
}
private boolean inRangeOfView(View view, MotionEvent ev) {
int []location = new int[2];
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
if (ev.getX() < x
|| ev.getX() > (x + view.getWidth())
|| ev.getY() < y
|| ev.getY() > (y + view.getHeight())) {
return false;
}
return true;
}
private void initResources() {
mHeadViewPanel = findViewById(R.id.note_title);
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv = (ImageView) findViewById(id);
iv.setOnClickListener(this);
}
mFontSizeSelector = findViewById(R.id.font_size_selector);
for (int id : sFontSizeBtnsMap.keySet()) {
View view = findViewById(id);
view.setOnClickListener(this);
};
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
/**
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
}
@Override
protected void onPause() {
super.onPause();
if(saveNote()) {
Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length());
}
clearSettingState();
}
private void updateWidget() {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class);
} else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) {
intent.setClass(this, NoteWidgetProvider_4x.class);
} else {
Log.e(TAG, "Unspported widget type");
return;
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
mWorkingNote.getWidgetId()
});
sendBroadcast(intent);
setResult(RESULT_OK, intent);
}
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
- View.VISIBLE);
} else if (sBgSelectorBtnsMap.containsKey(id)) {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.GONE);
mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id));
mNoteBgColorSelector.setVisibility(View.GONE);
} else if (sFontSizeBtnsMap.containsKey(id)) {
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE);
mFontSizeId = sFontSizeBtnsMap.get(id);
mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit();
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
getWorkingText();
switchToListMode(mWorkingNote.getContent());
} else {
mNoteEditor.setTextAppearance(this,
TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
}
mFontSizeSelector.setVisibility(View.GONE);
}
}
@Override
public void onBackPressed() {
if(clearSettingState()) {
return;
}
saveNote();
super.onBackPressed();
}
private boolean clearSettingState() {
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) {
mNoteBgColorSelector.setVisibility(View.GONE);
return true;
} else if (mFontSizeSelector.getVisibility() == View.VISIBLE) {
mFontSizeSelector.setVisibility(View.GONE);
return true;
}
return false;
}
public void onBackgroundColorChanged() {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.VISIBLE);
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (isFinishing()) {
return true;
}
clearSettingState();
menu.clear();
if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_note_edit, menu);
} else {
getMenuInflater().inflate(R.menu.note_edit, menu);
}
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
} else {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode);
}
if (mWorkingNote.hasClockAlert()) {
menu.findItem(R.id.menu_alert).setVisible(false);
} else {
menu.findItem(R.id.menu_delete_remind).setVisible(false);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_new_note:
createNewNote();
break;
case R.id.menu_delete:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.alert_message_delete_note));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
deleteCurrentNote();
finish();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
case R.id.menu_font_size:
mFontSizeSelector.setVisibility(View.VISIBLE);
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
break;
case R.id.menu_list_mode:
mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?
TextNote.MODE_CHECK_LIST : 0);
break;
case R.id.menu_share:
getWorkingText();
sendTo(this, mWorkingNote.getContent());
break;
case R.id.menu_send_to_desktop:
sendToDesktop();
break;
case R.id.menu_alert:
setReminder();
break;
case R.id.menu_delete_remind:
mWorkingNote.setAlertDate(0, false);
break;
default:
break;
}
return true;
}
private void setReminder() {
DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis());
d.setOnDateTimeSetListener(new OnDateTimeSetListener() {
public void OnDateTimeSet(AlertDialog dialog, long date) {
mWorkingNote.setAlertDate(date , true);
}
});
d.show();
}
/**
* Share note to apps that support {@link Intent#ACTION_SEND} action
* and {@text/plain} type
*/
private void sendTo(Context context, String info) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, info);
intent.setType("text/plain");
context.startActivity(intent);
}
private void createNewNote() {
// Firstly, save current editing notes
saveNote();
// For safety, start a new NoteEditActivity
finish();
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId());
startActivity(intent);
}
private void deleteCurrentNote() {
if (mWorkingNote.existInDatabase()) {
HashSet<Long> ids = new HashSet<Long>();
long id = mWorkingNote.getNoteId();
if (id != Notes.ID_ROOT_FOLDER) {
ids.add(id);
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
if (!isSyncMode()) {
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
Log.e(TAG, "Delete Note error");
}
} else {
if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
}
mWorkingNote.markDeleted(true);
}
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
public void onClockAlertChanged(long date, boolean set) {
/**
* User could set clock to an unsaved note, so before setting the
* alert clock, we should save the note first
*/
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if(!set) {
alarmManager.cancel(pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
}
} else {
/**
* There is the condition that user has input nothing (the note is
* not worthy saving), we have no note id, remind the user that he
* should input something
*/
Log.e(TAG, "Clock alert setting error");
showToast(R.string.error_note_empty_for_clock);
}
}
public void onWidgetChanged() {
updateWidget();
}
public void onEditTextDelete(int index, String text) {
int childCount = mEditTextList.getChildCount();
if (childCount == 1) {
return;
}
for (int i = index + 1; i < childCount; i++) {
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i - 1);
}
mEditTextList.removeViewAt(index);
NoteEditText edit = null;
if(index == 0) {
edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(
R.id.et_edit_text);
} else {
edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(
R.id.et_edit_text);
}
int length = edit.length();
edit.append(text);
edit.requestFocus();
edit.setSelection(length);
}
public void onEditTextEnter(int index, String text) {
/**
* Should not happen, check for debug
*/
if(index > mEditTextList.getChildCount()) {
Log.e(TAG, "Index out of mEditTextList boundrary, should not happen");
}
View view = getListItem(text, index);
mEditTextList.addView(view, index);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.requestFocus();
edit.setSelection(0);
for (int i = index + 1; i < mEditTextList.getChildCount(); i++) {
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i);
}
}
private void switchToListMode(String text) {
mEditTextList.removeAllViews();
String[] items = text.split("\n");
int index = 0;
for (String item : items) {
if(!TextUtils.isEmpty(item)) {
mEditTextList.addView(getListItem(item, index));
index++;
}
}
mEditTextList.addView(getListItem("", index));
mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus();
mNoteEditor.setVisibility(View.GONE);
mEditTextList.setVisibility(View.VISIBLE);
}
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
if (!TextUtils.isEmpty(userQuery)) {
mPattern = Pattern.compile(userQuery);
Matcher m = mPattern.matcher(fullText);
int start = 0;
while (m.find(start)) {
spannable.setSpan(
new BackgroundColorSpan(this.getResources().getColor(
R.color.user_query_highlight)), m.start(), m.end(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
start = m.end();
}
}
return spannable;
}
private View getListItem(String item, int index) {
View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null);
final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item));
cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
}
}
});
if (item.startsWith(TAG_CHECKED)) {
cb.setChecked(true);
edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
item = item.substring(TAG_CHECKED.length(), item.length()).trim();
} else if (item.startsWith(TAG_UNCHECKED)) {
cb.setChecked(false);
edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
item = item.substring(TAG_UNCHECKED.length(), item.length()).trim();
}
edit.setOnTextViewChangeListener(this);
edit.setIndex(index);
edit.setText(getHighlightQueryResult(item, mUserQuery));
return view;
}
public void onTextChange(int index, boolean hasText) {
if (index >= mEditTextList.getChildCount()) {
Log.e(TAG, "Wrong index, should not happen");
return;
}
if(hasText) {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE);
} else {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE);
}
}
public void onCheckListModeChanged(int oldMode, int newMode) {
if (newMode == TextNote.MODE_CHECK_LIST) {
switchToListMode(mNoteEditor.getText().toString());
} else {
if (!getWorkingText()) {
mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ",
""));
}
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
}
private boolean getWorkingText() {
boolean hasChecked = false;
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mEditTextList.getChildCount(); i++) {
View view = mEditTextList.getChildAt(i);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
if (!TextUtils.isEmpty(edit.getText())) {
if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) {
sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n");
hasChecked = true;
} else {
sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n");
}
}
}
mWorkingNote.setWorkingText(sb.toString());
} else {
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
}
return hasChecked;
}
private boolean saveNote() {
getWorkingText();
boolean saved = mWorkingNote.saveNote();
if (saved) {
/**
* There are two modes from List view to edit view, open one note,
* create/edit a node. Opening node requires to the original
* position in the list when back from edit view, while creating a
* new node requires to the top of the list. This code
* {@link #RESULT_OK} is used to identify the create/edit state
*/
setResult(RESULT_OK);
}
return saved;
}
private void sendToDesktop() {
/**
* Before send message to home, we should make sure that current
* editing note is exists in databases. So, for new note, firstly
* save it
*/
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
Intent sender = new Intent();
Intent shortcutIntent = new Intent(this, NoteEditActivity.class);
shortcutIntent.setAction(Intent.ACTION_VIEW);
shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId());
sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
sender.putExtra(Intent.EXTRA_SHORTCUT_NAME,
makeShortcutIconTitle(mWorkingNote.getContent()));
sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app));
sender.putExtra("duplicate", true);
sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
showToast(R.string.info_note_enter_desktop);
sendBroadcast(sender);
} else {
/**
* There is the condition that user has input nothing (the note is
* not worthy saving), we have no note id, remind the user that he
* should input something
*/
Log.e(TAG, "Send to desktop error");
showToast(R.string.error_note_empty_for_send_to_desktop);
}
}
private String makeShortcutIconTitle(String content) {
content = content.replace(TAG_CHECKED, "");
content = content.replace(TAG_UNCHECKED, "");
return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0,
SHORTCUT_ICON_TITLE_MAX_LEN) : content;
}
private void showToast(int resId) {
showToast(resId, Toast.LENGTH_SHORT);
}
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
}
*

@ -1,19 +1,3 @@
/*
* 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;
@ -37,15 +21,23 @@ import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
// NoteEditText类继承自EditText是一个自定义的文本编辑控件用于笔记编辑相关功能添加了一些特定的交互逻辑和事件处理。
public class NoteEditText extends EditText {
// 用于日志记录的TAG字符串方便在日志输出中识别该类相关的操作和信息便于调试。
private static final String TAG = "NoteEditText";
// 用于记录当前编辑文本在整个编辑列表中的索引位置,方便进行相关操作时定位和管理。
private int mIndex;
// 用于记录在删除操作前文本的选择起始位置,以便在合适的情况下触发相应的删除逻辑。
private int mSelectionStartBeforeDelete;
private static final String SCHEME_TEL = "tel:" ;
private static final String SCHEME_HTTP = "http:" ;
private static final String SCHEME_EMAIL = "mailto:" ;
// 定义电话号码链接的协议头字符串,用于识别文本中的电话号码链接。
private static final String SCHEME_TEL = "tel:";
// 定义超文本传输协议链接的协议头字符串,用于识别文本中的网页链接。
private static final String SCHEME_HTTP = "http:";
// 定义邮件链接的协议头字符串,用于识别文本中的邮件链接。
private static final String SCHEME_EMAIL = "mailto:";
// 一个静态的Map用于将不同的链接协议头字符串映射到对应的资源ID可能是用于显示相应链接类型的文本资源方便根据链接类型显示合适的提示信息等。
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
@ -53,109 +45,144 @@ public class NoteEditText extends EditText {
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
// 定义一个接口用于与外部如包含该NoteEditText的Activity等进行交互通知文本相关的变化事件如删除、添加文本以及文本内容改变等情况。
public interface OnTextViewChangeListener {
/**
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
* {@link KeyEvent#KEYCODE_DEL}
*
* @param index
* @param text ""
*/
void onEditTextDelete(int index, String text);
/**
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
* {@link KeyEvent#KEYCODE_ENTER}
*
* @param index + 1
* @param text
*/
void onEditTextEnter(int index, String text);
/**
* Hide or show item option when text change
*
*
* @param index
* @param hasText truefalse
*/
void onTextChange(int index, boolean hasText);
}
// 用于存储实现了OnTextViewChangeListener接口的监听器实例以便在相应事件发生时通知外部进行相关处理。
private OnTextViewChangeListener mOnTextViewChangeListener;
// 构造函数用于在只传入上下文的情况下创建NoteEditText实例默认设置索引为0。
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
// 设置当前编辑文本在列表中的索引位置的方法,外部可调用此方法来更新索引值。
public void setIndex(int index) {
mIndex = index;
}
// 设置OnTextViewChangeListener监听器的方法外部通过传入实现了该接口的实例来接收文本相关变化的通知。
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 构造函数用于在传入上下文和属性集的情况下创建NoteEditText实例采用默认的编辑文本样式通过指定android.R.attr.editTextStyle
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
// 构造函数用于在传入上下文、属性集和默认样式的情况下创建NoteEditText实例目前构造函数内暂时没有额外的自定义初始化逻辑TODO Auto-generated constructor stub部分可后续添加
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
// 重写onTouchEvent方法用于处理触摸事件这里主要处理了按下ACTION_DOWN动作实现了点击文本时准确设置文本选择位置的功能。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸点的原始X坐标相对于控件自身的坐标
int x = (int) event.getX();
// 获取触摸点的原始Y坐标相对于控件自身的坐标
int y = (int) event.getY();
// 减去控件总的左内边距,将坐标转换为相对于文本内容区域的坐标
x -= getTotalPaddingLeft();
// 减去控件总的上内边距,将坐标转换为相对于文本内容区域的坐标
y -= getTotalPaddingTop();
// 加上当前的滚动X偏移量考虑文本滚动情况得到准确的相对于文本内容的坐标
x += getScrollX();
// 加上当前的滚动Y偏移量考虑文本滚动情况得到准确的相对于文本内容的坐标
y += getScrollY();
// 获取文本的布局信息对象,用于后续根据坐标定位文本位置
Layout layout = getLayout();
// 根据触摸点的Y坐标获取所在的文本行号
int line = layout.getLineForVertical(y);
// 根据触摸点的X坐标和所在行号获取对应的文本偏移量字符位置
int off = layout.getOffsetForHorizontal(line, x);
// 设置文本的选择位置为计算得到的字符位置,实现点击准确选择文本的效果
Selection.setSelection(getText(), off);
break;
}
// 调用父类的onTouchEvent方法继续处理其他未处理的触摸事件逻辑或默认行为
return super.onTouchEvent(event);
}
// 重写onKeyDown方法用于处理按键按下事件这里主要针对回车键KEYCODE_ENTER和删除键KEYCODE_DEL进行了特定的处理记录相关状态等。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
// 如果设置了OnTextViewChangeListener监听器则返回false可能是让外部进一步处理回车键按下事件此处不做默认处理
if (mOnTextViewChangeListener!= null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录按下删除键时文本的选择起始位置,用于后续判断是否满足删除条件等操作
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
// 调用父类的onKeyDown方法继续处理其他未处理的按键按下事件逻辑或默认行为
return super.onKeyDown(keyCode, event);
}
// 重写onKeyUp方法用于处理按键抬起事件针对删除键KEYCODE_DEL和回车键KEYCODE_ENTER进行了具体的逻辑处理根据情况触发相应的文本操作并通知外部监听器。
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener != null) {
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
// 如果设置了OnTextViewChangeListener监听器
if (mOnTextViewChangeListener!= null) {
// 如果文本选择起始位置为0一般表示光标在文本开头且当前编辑文本索引不为0不是第一个编辑文本则认为符合删除条件
if (0 == mSelectionStartBeforeDelete && mIndex!= 0) {
// 调用监听器的onEditTextDelete方法通知外部删除当前编辑文本并返回true表示已处理该事件
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
// 如果未设置监听器,输出日志提示信息
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
if (mOnTextViewChangeListener!= null) {
// 获取文本选择的起始位置
int selectionStart = getSelectionStart();
// 获取从选择起始位置到文本末尾的内容,作为要添加的新文本内容(可能是回车键之后输入的新内容等)
String text = getText().subSequence(selectionStart, length()).toString();
// 将原文本截断为选择起始位置之前的内容,相当于去除了回车键之后的部分(准备添加新文本)
setText(getText().subSequence(0, selectionStart));
// 调用监听器的onEditTextEnter方法通知外部添加新的编辑文本并传入相应的索引和文本内容
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
@ -164,54 +191,69 @@ public class NoteEditText extends EditText {
default:
break;
}
// 调用父类的onKeyUp方法继续处理其他未处理的按键抬起事件逻辑或默认行为
return super.onKeyUp(keyCode, event);
}
// 重写onFocusChanged方法用于处理焦点变化事件根据焦点状态以及文本内容是否为空通知外部监听器文本是否有内容的变化情况。
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
if (mOnTextViewChangeListener!= null) {
// 如果失去焦点且文本内容为空调用监听器的onTextChange方法通知外部文本无内容了
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
// 否则有焦点或者文本有内容调用监听器的onTextChange方法通知外部文本有内容
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
// 调用父类的onFocusChanged方法继续处理其他未处理的焦点变化相关逻辑或默认行为
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
// 重写onCreateContextMenu方法用于创建上下文菜单长按文本等操作弹出的菜单这里主要是针对文本中的链接URLSpan类型创建相应的菜单项点击菜单项可触发链接跳转等操作。
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) {
// 获取文本选择的起始位置
int selStart = getSelectionStart();
// 获取文本选择的结束位置
int selEnd = getSelectionEnd();
// 获取选择范围的最小位置(起始和结束位置中较小的那个)
int min = Math.min(selStart, selEnd);
// 获取选择范围的最大位置(起始和结束位置中较大的那个)
int max = Math.max(selStart, selEnd);
// 获取在选择范围内的所有URLSpan链接相关的文本样式对象
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
// 遍历链接协议头与资源ID的映射表查找与当前链接匹配的资源ID根据链接的协议头来确定
for (String schema : sSchemaActionResMap.keySet()) {
if (urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
// 如果未找到匹配的资源ID可能是不常见的链接协议类型则设置默认的资源ID一般是用于显示“其他链接”之类的提示文本
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 添加一个菜单项到上下文菜单设置菜单项的相关属性如ID、顺序、显示文本等并设置点击菜单项的监听器点击时触发链接的点击操作一般是打开相应链接等
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// goto a new intent
// 调用URLSpan的onClick方法触发链接跳转等默认行为一般会启动相应的应用来处理链接如浏览器打开网页链接等
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
// 调用父类的onCreateContextMenu方法继续处理其他未处理的上下文菜单创建相关逻辑或默认行为
super.onCreateContextMenu(menu);
}
}
}

@ -1,19 +1,3 @@
/*
* 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;
@ -25,8 +9,9 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
// NoteItemData类用于封装从数据库游标Cursor中获取的笔记相关数据并提供一系列方法来获取这些数据以及判断笔记在列表中的相关位置状态等信息。
public class NoteItemData {
// 定义一个字符串数组用于指定从数据库查询笔记数据时需要获取的列名这些列对应着笔记的各种属性信息如ID、提醒日期、背景颜色ID等。
static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
@ -42,60 +27,98 @@ public class NoteItemData {
NoteColumns.WIDGET_TYPE,
};
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;
// 定义常量用于标识在游标结果集中各列对应的索引位置方便后续从游标中准确获取相应列的数据以下每个常量对应着上面PROJECTION数组中列名的顺序位置。
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;
// 笔记的类型,例如可能区分普通笔记、文件夹、系统相关笔记等不同类型,对应数据库中定义的类型标识。
private int mType;
// 笔记关联的桌面小部件的ID如果该笔记与桌面小部件有相关关联的话通过该ID可以找到对应的小部件。
private int mWidgetId;
// 笔记关联的桌面小部件的类型,可能用于区分不同样式、功能的桌面小部件,对应数据库中定义的小部件类型标识。
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) {
// 从游标中获取笔记的ID列数据赋值给mId成员变量。
mId = cursor.getLong(ID_COLUMN);
// 从游标中获取笔记的提醒日期列数据赋值给mAlertDate成员变量。
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
// 从游标中获取笔记的背景颜色ID列数据赋值给mBgColorId成员变量。
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
// 从游标中获取笔记的创建日期列数据赋值给mCreatedDate成员变量。
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
// 根据游标中获取的是否有附件列的数据转换为布尔值赋值给mHasAttachment成员变量大于0表示有附件赋值为true否则为false。
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0)? true : false;
// 从游标中获取笔记的最后修改日期列数据赋值给mModifiedDate成员变量。
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
// 从游标中获取笔记所在文件夹下的笔记数量列数据赋值给mNotesCount成员变量。
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
// 从游标中获取笔记所属父文件夹的ID列数据赋值给mParentId成员变量。
mParentId = cursor.getLong(PARENT_ID_COLUMN);
// 从游标中获取笔记内容的摘要信息列数据赋值给mSnippet成员变量并对摘要中的特定标记如勾选标记等进行替换处理去除这些标记得到更纯净的摘要文本。
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
// 从游标中获取笔记的类型列数据赋值给mType成员变量用于区分不同类型的笔记。
mType = cursor.getInt(TYPE_COLUMN);
// 从游标中获取笔记关联的桌面小部件的ID列数据赋值给mWidgetId成员变量。
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
// 从游标中获取笔记关联的桌面小部件的类型列数据赋值给mWidgetType成员变量。
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 初始化电话号码为空字符串,后续根据笔记是否是通话记录相关来获取具体的电话号码。
mPhoneNumber = "";
// 如果笔记所属父文件夹的ID是通话记录文件夹的特定ID表示该笔记是通话记录相关的笔记。
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 通过DataUtils工具类根据笔记ID从内容解析器ContentResolver中获取对应的电话号码。
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
// 如果获取到的电话号码不为空字符串,表示找到了对应的电话号码。
if (!TextUtils.isEmpty(mPhoneNumber)) {
// 通过Contact工具类根据电话号码获取对应的联系人姓名如果获取到的联系人姓名为null则直接使用电话号码作为姓名。
mName = Contact.getContact(context, mPhoneNumber);
if (mName == null) {
mName = mPhoneNumber;
@ -103,30 +126,43 @@ public class NoteItemData {
}
}
// 如果最终联系人姓名仍为null可能未获取到或者本身就不存在则设置为空字符串。
if (mName == null) {
mName = "";
}
// 调用checkPostion方法根据游标情况判断该笔记在列表或文件夹结构中的相关位置状态信息。
checkPostion(cursor);
}
// 私有方法用于根据数据库游标Cursor判断笔记在列表或文件夹结构中的相关位置状态信息如是否是最后一项、第一项、唯一一项以及在文件夹下的笔记数量关系等。
private void checkPostion(Cursor cursor) {
mIsLastItem = cursor.isLast() ? true : false;
mIsFirstItem = cursor.isFirst() ? true : false;
// 根据游标是否指向最后一条记录设置mIsLastItem为相应的布尔值表示是否是列表中的最后一项。
mIsLastItem = cursor.isLast()? true : false;
// 根据游标是否指向第一条记录设置mIsFirstItem为相应的布尔值表示是否是列表中的第一项。
mIsFirstItem = cursor.isFirst()? true : false;
// 根据游标获取的记录总数是否为1设置mIsOnlyOneItem为相应的布尔值表示列表中是否只有这一个笔记项。
mIsOnlyOneItem = (cursor.getCount() == 1);
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
// 如果笔记类型是普通笔记TYPE_NOTE且不是列表中的第一项表示前面可能有其他记录可能存在文件夹相关判断情况
if (mType == Notes.TYPE_NOTE &&!mIsFirstItem) {
// 获取当前游标所在的位置索引。
int position = cursor.getPosition();
// 将游标移动到前一条记录,以便查看前一条记录的类型等信息。
if (cursor.moveToPrevious()) {
// 判断前一条记录的类型是否是文件夹类型或者系统类型(可能意味着当前笔记是跟随在某个文件夹之后的情况)。
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;
}
}
// 将游标再移回原来的位置(恢复到当前笔记对应的游标位置),确保后续操作不受影响,如果无法移回则抛出异常。
if (!cursor.moveToNext()) {
throw new IllegalStateException("cursor move to previous but can't move back");
}
@ -134,91 +170,108 @@ public class NoteItemData {
}
}
// 判断该笔记是否是某个文件夹下仅跟随的一个笔记的方法,外部可调用此方法获取相应的位置状态信息。
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的方法外部可调用此方法获取笔记的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;
}
public long getParentId() {
// 获取笔记所属父文件夹的ID的方法外部可调用此方法获取笔记所属文件夹的信息与上面的getParentId方法作用相同可能为了代码逻辑清晰或不同场景调用方便提供两个同名方法
public long getFolderId () {
return mParentId;
}
// 获取笔记所在文件夹下的笔记数量的方法,外部可调用此方法获取相关的数量统计信息。
public int getNotesCount() {
return mNotesCount;
}
public long getFolderId () {
return mParentId;
}
// 获取笔记的类型的方法,外部可调用此方法获取笔记的类型标识信息,用于区分不同类型的笔记。
public int getType() {
return mType;
}
// 获取笔记关联的桌面小部件的类型的方法,外部可调用此方法获取桌面小部件的类型信息。
public int getWidgetType() {
return mWidgetType;
}
// 获取笔记关联的桌面小部件的ID的方法外部可调用此方法获取桌面小部件的具体ID信息。
public int getWidgetId() {
return mWidgetId;
}
// 获取笔记内容摘要信息的方法,外部可调用此方法获取笔记的简短预览内容信息。
public String getSnippet() {
return mSnippet;
}
// 判断笔记是否设置了提醒根据提醒日期是否大于0来判断的方法外部可调用此方法获取笔记是否有提醒设置的信息。
public boolean hasAlert() {
return (mAlertDate > 0);
}
// 判断笔记是否是通话记录相关笔记的方法通过判断所属父文件夹ID以及电话号码是否为空来确定外部可调用此方法获取笔记是否是通话记录类型的信息。
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
return (mParentId == Notes.ID_CALL_RECORD_FOLDER &&!TextUtils.isEmpty(mPhoneNumber));
}
// 静态方法用于从给定的数据库游标Cursor中获取笔记的类型信息外部可直接调用此方法获取游标中对应笔记的类型无需创建NoteItemData实例。
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}
}

@ -1,19 +1,3 @@
/*
* 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.app.Activity;
@ -78,63 +62,72 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
// NotesListActivity类是一个Android的Activity主要用于展示笔记列表处理与笔记列表相关的各种用户交互操作
// 如点击、长按、添加新笔记、删除笔记、移动笔记、文件夹操作以及与桌面小部件的交互等功能。
public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener {
// 用于标识查询文件夹内笔记列表的请求标记,在异步查询操作中作为区分不同查询任务的标识。
private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;
private static final int FOLDER_LIST_QUERY_TOKEN = 1;
// 用于标识查询文件夹列表的请求标记,同样在异步查询操作中起区分作用。
private static final int FOLDER_LIST_QUERY_TOKEN = 1;
// 菜单中删除文件夹选项对应的ID。
private static final int MENU_FOLDER_DELETE = 0;
// 菜单中查看文件夹选项对应的ID。
private static final int MENU_FOLDER_VIEW = 1;
// 菜单中修改文件夹名称选项对应的ID。
private static final int MENU_FOLDER_CHANGE_NAME = 2;
// 用于存储是否已添加应用介绍的偏好设置的键值,用于判断是否首次使用应用并展示相应介绍信息。
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
// 定义一个枚举类型,表示列表编辑的不同状态,例如普通笔记列表、子文件夹、通话记录文件夹等不同情况。
private enum ListEditState {
NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
};
// 当前列表的编辑状态,用于在不同操作中判断所处的具体场景并执行相应逻辑。
private ListEditState mState;
// 用于处理后台数据库查询的异步查询处理器,方便在后台线程进行数据查询操作,避免阻塞主线程。
private BackgroundQueryHandler mBackgroundQueryHandler;
// 笔记列表的适配器用于将数据绑定到列表视图ListView管理笔记数据在列表中的展示等相关逻辑。
private NotesListAdapter mNotesListAdapter;
// 显示笔记列表的ListView控件用户通过该控件查看和操作笔记列表。
private ListView mNotesListView;
// “添加新笔记”按钮,用户点击该按钮可以创建新的笔记。
private Button mAddNewNote;
// 用于标记是否进行事件分发的布尔值,在处理触摸事件等场景中控制事件流向。
private boolean mDispatch;
// 记录触摸事件起始的Y坐标用于触摸事件相关的位置判断和逻辑处理。
private int mOriginY;
// 记录触摸事件分发的目标Y坐标同样用于触摸事件相关处理。
private int mDispatchY;
// 标题栏的TextView控件用于显示当前所在文件夹等相关的标题信息。
private TextView mTitleBar;
// 当前所在文件夹的ID用于标识当前操作的笔记所属的文件夹方便进行数据查询和操作定位。
private long mCurrentFolderId;
// 用于访问内容提供器Content Provider的对象通过它可以与数据库等数据源进行交互获取和操作笔记数据。
private ContentResolver mContentResolver;
// 用于处理ListView的多选模式相关逻辑的回调对象实现了多选模式下的各种菜单操作、状态变化等逻辑。
private ModeCallback mModeCallBack;
// 用于日志记录的TAG字符串方便在日志输出中识别该类相关的操作和信息便于调试。
private static final String TAG = "NotesListActivity";
// 定义笔记列表视图滚动的速率,可能用于控制列表滚动的速度等相关逻辑(具体使用场景需看代码中其他地方的调用情况)。
public static final int NOTES_LISTVIEW_SCROLL_RATE = 30;
// 用于存储当前获取焦点的笔记数据项,方便在长按等操作中获取对应的笔记详细信息进行后续处理。
private NoteItemData mFocusNoteDataItem;
// 定义查询普通文件夹内笔记的条件语句字符串通过指定父文件夹ID来筛选相应的笔记记录。
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
// 定义查询根文件夹下笔记的条件语句字符串,包含了筛选普通笔记以及通话记录文件夹下有笔记的情况等复杂条件。
private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
+ Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR ("
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND "
+ NoteColumns.NOTES_COUNT + ">0)";
// 定义打开已有笔记的请求代码,用于在`onActivityResult`方法中区分不同的启动Activity返回结果情况。
private final static int REQUEST_CODE_OPEN_NODE = 102;
private final static int REQUEST_CODE_NEW_NODE = 103;
// 定义新建笔记的请求代码,同样用于在`onActivityResult`方法中区分不同情况。
private final static int REQUEST_CODE_NEW_NODE = 103;
// Activity创建时调用的方法进行一些初始化操作如设置布局、初始化资源等。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -142,11 +135,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
initResources();
/**
* Insert an introduction when user firstly use this application
* 使
*/
setAppInfoFromRawRes();
}
// 处理从其他Activity返回的结果根据请求代码和返回结果状态进行相应的操作比如更新笔记列表适配器等。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK
@ -157,18 +151,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
// 从原始资源文件中读取应用介绍内容,并创建一个笔记保存该介绍信息,同时设置相应的偏好设置表示已添加过介绍,
// 下次启动应用时就不会再重复添加介绍内容了。
private void setAppInfoFromRawRes() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
try {
in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
// 打开应用内的原始资源文件(介绍信息文件),获取输入流用于读取文件内容。
in = getResources().openRawResource(R.raw.introduction);
if (in!= null) {
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
char [] buf = new char[1024];
char[] buf = new char[1024];
int len = 0;
// 逐字符读取文件内容添加到StringBuilder中直到读取完整个文件。
while ((len = br.read(buf)) > 0) {
sb.append(buf, 0, len);
}
@ -180,20 +178,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
e.printStackTrace();
return;
} finally {
if(in != null) {
if (in!= null) {
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
// 关闭输入流,如果出现异常则打印堆栈信息,确保资源能尽量正确释放。
e.printStackTrace();
}
}
}
// 创建一个空的工作笔记对象,设置相关属性(如所属文件夹、小部件相关信息等),并将读取到的介绍内容设置为笔记的文本内容。
WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER,
AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE,
ResourceParser.RED);
note.setWorkingText(sb.toString());
// 保存笔记到数据库,如果保存成功则更新偏好设置,表示已添加过介绍信息;若保存失败则记录日志错误信息。
if (note.saveNote()) {
sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit();
} else {
@ -203,20 +203,25 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
// Activity启动时调用的方法在这里启动异步的笔记列表查询操作获取并展示初始的笔记列表数据。
@Override
protected void onStart() {
super.onStart();
startAsyncNotesListQuery();
}
// 初始化各种资源对象如内容解析器、异步查询处理器、ListView及其相关监听器、适配器、按钮等控件以及设置初始状态等信息。
private void initResources() {
mContentResolver = this.getContentResolver();
mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver());
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mNotesListView = (ListView) findViewById(R.id.notes_list);
// 给ListView添加页脚视图用于展示一些额外的信息或者操作按钮等此处从布局文件中加载相应的视图并添加
mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null),
null, false);
// 设置ListView的点击事件监听器当用户点击列表项时触发相应的操作逻辑。
mNotesListView.setOnItemClickListener(new OnListItemClickListener());
// 设置ListView的长按事件监听器当用户长按列表项时触发相应的操作逻辑此处设置为当前Activity自身因为它实现了该监听器接口。
mNotesListView.setOnItemLongClickListener(this);
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
@ -231,11 +236,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mModeCallBack = new ModeCallback();
}
// 内部类实现了ListView的多选模式相关的各种回调方法用于处理多选模式下的菜单创建、菜单项点击、状态变化等操作逻辑。
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
private DropdownMenu mDropDownMenu;
private ActionMode mActionMode;
private MenuItem mMoveMenu;
// 创建多选模式下的操作菜单,加载相应的菜单布局文件,并设置菜单项的点击监听器等,同时进行一些界面相关的初始化操作,比如隐藏添加新笔记按钮等。
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
@ -258,7 +265,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mDropDownMenu = new DropdownMenu(NotesListActivity.this,
(Button) customView.findViewById(R.id.selection_menu),
R.menu.note_list_dropdown);
mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected());
updateMenu();
@ -269,13 +276,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
return true;
}
// 根据已选择的笔记数量更新下拉菜单的标题和选择全部/取消选择全部菜单项的状态及标题等信息,用于展示实时的选择情况。
private void updateMenu() {
int selectedCount = mNotesListAdapter.getSelectedCount();
// Update dropdown menu
// 更新下拉菜单标题,显示已选择的笔记数量相关的提示信息。
String format = getResources().getString(R.string.menu_select_title, selectedCount);
mDropDownMenu.setTitle(format);
MenuItem item = mDropDownMenu.findItem(R.id.action_select_all);
if (item != null) {
if (item!= null) {
if (mNotesListAdapter.isAllSelected()) {
item.setChecked(true);
item.setTitle(R.string.menu_deselect_all);
@ -286,32 +294,38 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
// 准备多选模式下的操作菜单,目前该方法暂未实现具体逻辑(可根据需要在后续添加相关预处理逻辑)。
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// TODO Auto-generated method stub
return false;
}
// 处理多选模式下点击菜单项的操作,目前该方法暂未完整实现具体逻辑(可根据需要在后续添加不同菜单项点击后的具体处理逻辑)。
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// TODO Auto-generated method stub
return false;
}
// 销毁多选模式时调用的方法恢复相关的界面状态和适配器状态比如重新显示添加新笔记按钮、设置ListView可长按等。
public void onDestroyActionMode(ActionMode mode) {
mNotesListAdapter.setChoiceMode(false);
mNotesListView.setLongClickable(true);
mAddNewNote.setVisibility(View.VISIBLE);
}
// 完成多选模式操作,关闭当前的操作模式,触发`onDestroyActionMode`等相关逻辑。
public void finishActionMode() {
mActionMode.finish();
}
// 当列表项的选中状态发生变化时调用的方法,更新适配器中对应项的选中状态,并调用`updateMenu`方法更新菜单显示状态。
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
mNotesListAdapter.setCheckedItem(position, checked);
updateMenu();
}
// 处理菜单项点击事件,根据点击的不同菜单项执行相应的操作,比如删除所选笔记、移动所选笔记等,同时进行一些必要的提示和验证操作。
public boolean onMenuItemClick(MenuItem item) {
if (mNotesListAdapter.getSelectedCount() == 0) {
Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none),
@ -323,632 +337,4 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case R.id.delete:
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.alert_message_delete_notes,
mNotesListAdapter.getSelectedCount()));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
batchDelete();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
case R.id.move:
startQueryDestinationFolders();
break;
default:
return false;
}
return true;
}
}
private class NewNoteOnTouchListener implements OnTouchListener {
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Display display = getWindowManager().getDefaultDisplay();
int screenHeight = display.getHeight();
int newNoteViewHeight = mAddNewNote.getHeight();
int start = screenHeight - newNoteViewHeight;
int eventY = start + (int) event.getY();
/**
* Minus TitleBar's height
*/
if (mState == ListEditState.SUB_FOLDER) {
eventY -= mTitleBar.getHeight();
start -= mTitleBar.getHeight();
}
/**
* HACKME:When click the transparent part of "New Note" button, dispatch
* the event to the list view behind this button. The transparent part of
* "New Note" button could be expressed by formula y=-0.12x+94Unit:pixel
* and the line top of the button. The coordinate based on left of the "New
* Note" button. The 94 represents maximum height of the transparent part.
* Notice that, if the background of the button changes, the formula should
* also change. This is very bad, just for the UI designer's strong requirement.
*/
if (event.getY() < (event.getX() * (-0.12) + 94)) {
View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1
- mNotesListView.getFooterViewsCount());
if (view != null && view.getBottom() > start
&& (view.getTop() < (start + 94))) {
mOriginY = (int) event.getY();
mDispatchY = eventY;
event.setLocation(event.getX(), mDispatchY);
mDispatch = true;
return mNotesListView.dispatchTouchEvent(event);
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDispatch) {
mDispatchY += (int) event.getY() - mOriginY;
event.setLocation(event.getX(), mDispatchY);
return mNotesListView.dispatchTouchEvent(event);
}
break;
}
default: {
if (mDispatch) {
event.setLocation(event.getX(), mDispatchY);
mDispatch = false;
return mNotesListView.dispatchTouchEvent(event);
}
break;
}
}
return false;
}
};
private void startAsyncNotesListQuery() {
String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
: NORMAL_SELECTION;
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
String.valueOf(mCurrentFolderId)
}, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
public BackgroundQueryHandler(ContentResolver contentResolver) {
super(contentResolver);
}
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
switch (token) {
case FOLDER_NOTE_LIST_QUERY_TOKEN:
mNotesListAdapter.changeCursor(cursor);
break;
case FOLDER_LIST_QUERY_TOKEN:
if (cursor != null && cursor.getCount() > 0) {
showFolderListMenu(cursor);
} else {
Log.e(TAG, "Query folder failed");
}
break;
default:
return;
}
}
}
private void showFolderListMenu(Cursor cursor) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(R.string.menu_title_select_folder);
final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor);
builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
DataUtils.batchMoveToFolder(mContentResolver,
mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which));
Toast.makeText(
NotesListActivity.this,
getString(R.string.format_move_notes_to_folder,
mNotesListAdapter.getSelectedCount(),
adapter.getFolderName(NotesListActivity.this, which)),
Toast.LENGTH_SHORT).show();
mModeCallBack.finishActionMode();
}
});
builder.show();
}
private void createNewNote() {
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId);
this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
}
private void batchDelete() {
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
if (!isSyncMode()) {
// if not synced, delete notes directly
if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
.getSelectedItemIds())) {
} else {
Log.e(TAG, "Delete notes error, should not happens");
}
} else {
// in sync mode, we'll move the deleted note into the trash
// folder
if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
return widgets;
}
@Override
protected void onPostExecute(HashSet<AppWidgetAttribute> widgets) {
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType);
}
}
}
mModeCallBack.finishActionMode();
}
}.execute();
}
private void deleteFolder(long folderId) {
if (folderId == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Wrong folder id, should not happen " + folderId);
return;
}
HashSet<Long> ids = new HashSet<Long>();
ids.add(folderId);
HashSet<AppWidgetAttribute> widgets = DataUtils.getFolderNoteWidget(mContentResolver,
folderId);
if (!isSyncMode()) {
// if not synced, delete folder directly
DataUtils.batchDeleteNotes(mContentResolver, ids);
} else {
// in sync mode, we'll move the deleted folder into the trash folder
DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);
}
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType);
}
}
}
}
private void openNode(NoteItemData data) {
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, data.getId());
this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
}
private void openFolder(NoteItemData data) {
mCurrentFolderId = data.getId();
startAsyncNotesListQuery();
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
mAddNewNote.setVisibility(View.GONE);
} else {
mState = ListEditState.SUB_FOLDER;
}
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mTitleBar.setText(R.string.call_record_folder_name);
} else {
mTitleBar.setText(data.getSnippet());
}
mTitleBar.setVisibility(View.VISIBLE);
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_new_note:
createNewNote();
break;
default:
break;
}
}
private void showSoftInput() {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
}
private void hideSoftInput(View view) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
private void showCreateOrModifyFolderDialog(final boolean create) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
showSoftInput();
if (!create) {
if (mFocusNoteDataItem != null) {
etName.setText(mFocusNoteDataItem.getSnippet());
builder.setTitle(getString(R.string.menu_folder_change_name));
} else {
Log.e(TAG, "The long click data item is null");
return;
}
} else {
etName.setText("");
builder.setTitle(this.getString(R.string.menu_create_folder));
}
builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
hideSoftInput(etName);
}
});
final Dialog dialog = builder.setView(view).show();
final Button positive = (Button)dialog.findViewById(android.R.id.button1);
positive.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
hideSoftInput(etName);
String name = etName.getText().toString();
if (DataUtils.checkVisibleFolderName(mContentResolver, name)) {
Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name),
Toast.LENGTH_LONG).show();
etName.setSelection(0, etName.length());
return;
}
if (!create) {
if (!TextUtils.isEmpty(name)) {
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID
+ "=?", new String[] {
String.valueOf(mFocusNoteDataItem.getId())
});
}
} else if (!TextUtils.isEmpty(name)) {
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
mContentResolver.insert(Notes.CONTENT_NOTE_URI, values);
}
dialog.dismiss();
}
});
if (TextUtils.isEmpty(etName.getText())) {
positive.setEnabled(false);
}
/**
* When the name edit text is null, disable the positive button
*/
etName.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// TODO Auto-generated method stub
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (TextUtils.isEmpty(etName.getText())) {
positive.setEnabled(false);
} else {
positive.setEnabled(true);
}
}
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
}
});
}
@Override
public void onBackPressed() {
switch (mState) {
case SUB_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
startAsyncNotesListQuery();
mTitleBar.setVisibility(View.GONE);
break;
case CALL_RECORD_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
mAddNewNote.setVisibility(View.VISIBLE);
mTitleBar.setVisibility(View.GONE);
startAsyncNotesListQuery();
break;
case NOTE_LIST:
super.onBackPressed();
break;
default:
break;
}
}
private void updateWidget(int appWidgetId, int appWidgetType) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
if (appWidgetType == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class);
} else if (appWidgetType == Notes.TYPE_WIDGET_4X) {
intent.setClass(this, NoteWidgetProvider_4x.class);
} else {
Log.e(TAG, "Unspported widget type");
return;
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
appWidgetId
});
sendBroadcast(intent);
setResult(RESULT_OK, intent);
}
private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (mFocusNoteDataItem != null) {
menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete);
menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name);
}
}
};
@Override
public void onContextMenuClosed(Menu menu) {
if (mNotesListView != null) {
mNotesListView.setOnCreateContextMenuListener(null);
}
super.onContextMenuClosed(menu);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (mFocusNoteDataItem == null) {
Log.e(TAG, "The long click data item is null");
return false;
}
switch (item.getItemId()) {
case MENU_FOLDER_VIEW:
openFolder(mFocusNoteDataItem);
break;
case MENU_FOLDER_DELETE:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.alert_message_delete_folder));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
deleteFolder(mFocusNoteDataItem.getId());
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
case MENU_FOLDER_CHANGE_NAME:
showCreateOrModifyFolderDialog(false);
break;
default:
break;
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear();
if (mState == ListEditState.NOTE_LIST) {
getMenuInflater().inflate(R.menu.note_list, menu);
// set sync or sync_cancel
menu.findItem(R.id.menu_sync).setTitle(
GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
} else if (mState == ListEditState.SUB_FOLDER) {
getMenuInflater().inflate(R.menu.sub_folder, menu);
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_record_folder, menu);
} else {
Log.e(TAG, "Wrong state:" + mState);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_new_folder: {
showCreateOrModifyFolderDialog(true);
break;
}
case R.id.menu_export_text: {
exportNoteToText();
break;
}
case R.id.menu_sync: {
if (isSyncMode()) {
if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
GTaskSyncService.startSync(this);
} else {
GTaskSyncService.cancelSync(this);
}
} else {
startPreferenceActivity();
}
break;
}
case R.id.menu_setting: {
startPreferenceActivity();
break;
}
case R.id.menu_new_note: {
createNewNote();
break;
}
case R.id.menu_search:
onSearchRequested();
break;
default:
break;
}
return true;
}
@Override
public boolean onSearchRequested() {
startSearch(null, false, null /* appData */, false);
return true;
}
private void exportNoteToText() {
final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this);
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... unused) {
return backup.exportToText();
}
@Override
protected void onPostExecute(Integer result) {
if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(NotesListActivity.this
.getString(R.string.failed_sdcard_export));
builder.setMessage(NotesListActivity.this
.getString(R.string.error_sdcard_unmounted));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
} else if (result == BackupUtils.STATE_SUCCESS) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(NotesListActivity.this
.getString(R.string.success_sdcard_export));
builder.setMessage(NotesListActivity.this.getString(
R.string.format_exported_file_location, backup
.getExportedTextFileName(), backup.getExportedTextFileDir()));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
} else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(NotesListActivity.this
.getString(R.string.failed_sdcard_export));
builder.setMessage(NotesListActivity.this
.getString(R.string.error_sdcard_export));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
}
}
}.execute();
}
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
private void startPreferenceActivity() {
Activity from = getParent() != null ? getParent() : this;
Intent intent = new Intent(from, NotesPreferenceActivity.class);
from.startActivityIfNeeded(intent, -1);
}
private class OnListItemClickListener implements OnItemClickListener {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (view instanceof NotesListItem) {
NoteItemData item = ((NotesListItem) view).getItemData();
if (mNotesListAdapter.isInChoiceMode()) {
if (item.getType() == Notes.TYPE_NOTE) {
position = position - mNotesListView.getHeaderViewsCount();
mModeCallBack.onItemCheckedStateChanged(null, position, id,
!mNotesListAdapter.isSelectedItem(position));
}
return;
}
switch (mState) {
case NOTE_LIST:
if (item.getType() == Notes.TYPE_FOLDER
|| item.getType() == Notes.TYPE_SYSTEM) {
openFolder(item);
} else if (item.getType() == Notes.TYPE_NOTE) {
openNode(item);
} else {
Log.e(TAG, "Wrong note type in NOTE_LIST");
}
break;
case SUB_FOLDER:
case CALL_RECORD_FOLDER:
if (item.getType() == Notes.TYPE_NOTE) {
openNode(item);
} else {
Log.e(TAG, "Wrong note type in SUB_FOLDER");
}
break;
default:
break;
}
}
}
}
private void startQueryDestinationFolders() {
String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?";
selection = (mState == ListEditState.NOTE_LIST) ? selection:
"(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN,
null,
Notes.CONTENT_NOTE_URI,
FoldersListAdapter.PROJECTION,
selection,
new String[] {
String.valueOf(Notes.TYPE_FOLDER),
String.valueOf(Notes.ID_TRASH_FOLER),
String.valueOf(mCurrentFolderId)
},
NoteColumns.MODIFIED_DATE + " DESC");
}
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
if (view instanceof NotesListItem) {
mFocusNoteDataItem = ((NotesListItem) view).getItemData();
if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
if (mNotesListView.startActionMode(mModeCallBack) != null) {
mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
} else {
Log.e(TAG, "startActionMode fails");
}
} else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
}
}
return false;
}
}
builder.setIcon(android.R.drawable.ic_dialog_

@ -1,19 +1,3 @@
/*
* 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;
@ -30,19 +14,27 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
// NotesListAdapter类继承自CursorAdapter用于将数据库游标Cursor中的数据适配到ListView等可展示列表数据的控件上
// 同时还管理了笔记列表项的选中状态等相关逻辑,方便在多选等操作场景下使用。
public class NotesListAdapter extends CursorAdapter {
// 用于日志记录的TAG字符串方便在日志输出中识别该类相关的操作和信息便于调试。
private static final String TAG = "NotesListAdapter";
// 保存上下文信息用于后续创建视图、获取资源等操作通常是关联的Activity或其他Context对象。
private Context mContext;
// 使用HashMap来存储笔记列表项的选中状态键为列表项的位置索引值为布尔类型表示是否被选中。
private HashMap<Integer, Boolean> mSelectedIndex;
// 记录笔记的数量(可能是符合特定条件的笔记数量,比如普通笔记的数量等,具体取决于代码中的计算逻辑)。
private int mNotesCount;
// 表示是否处于多选模式的布尔值,用于控制列表的交互行为以及相关界面显示等情况。
private boolean mChoiceMode;
// 内部静态类用于封装桌面小部件的相关属性包含小部件的ID和类型信息方便在涉及小部件与笔记关联的操作中传递和使用这些属性。
public static class AppWidgetAttribute {
public int widgetId;
public int widgetType;
};
// 构造函数初始化NotesListAdapter实例调用父类构造函数并传入上下文和初始空游标同时初始化选中状态的HashMap以及其他相关成员变量。
public NotesListAdapter(Context context) {
super(context, null);
mSelectedIndex = new HashMap<Integer, Boolean>();
@ -50,11 +42,14 @@ public class NotesListAdapter extends CursorAdapter {
mNotesCount = 0;
}
// 创建新的列表项视图,在该方法中创建并返回一个`NotesListItem`类型的视图,该视图会在列表滚动等需要新增显示项时被调用创建。
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context);
}
// 绑定数据到已创建的列表项视图上,将游标中的数据解析封装为`NoteItemData`对象,并传递给`NotesListItem`视图进行具体的数据展示绑定,
// 同时传入多选模式状态以及该项是否被选中的信息,以便视图根据这些情况进行相应的展示调整。
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) {
@ -64,20 +59,26 @@ public class NotesListAdapter extends CursorAdapter {
}
}
// 设置指定位置的列表项的选中状态,将对应的选中状态存入`mSelectedIndex` HashMap中并通知数据集已更改触发视图更新来反映选中状态变化。
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
// 判断当前是否处于多选模式,返回`mChoiceMode`成员变量的值,外部可通过调用该方法了解列表当前的交互模式状态。
public boolean isInChoiceMode() {
return mChoiceMode;
}
// 设置列表的多选模式状态,清除已有的选中状态记录(通过清空`mSelectedIndex` HashMap并更新`mChoiceMode`成员变量的值,
// 外部可调用该方法开启或关闭多选模式,并重置相关选中状态。
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
mChoiceMode = mode;
}
// 全选或全不选所有符合条件(通常是笔记类型为普通笔记的项)的列表项,遍历游标中的所有数据项,根据笔记类型判断是否设置其选中状态,
// 从而实现全选或全不选功能,方便用户在多选模式下快速操作。
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
@ -89,6 +90,8 @@ public class NotesListAdapter extends CursorAdapter {
}
}
// 获取所有已选中列表项的ID集合遍历`mSelectedIndex` HashMap中已选中的项值为`true`的项),通过`getItemId`方法获取对应的ID
// 并添加到`HashSet`集合中返回,可用于后续批量操作(如删除、移动等)已选中的笔记项。
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
@ -105,12 +108,15 @@ public class NotesListAdapter extends CursorAdapter {
return itemSet;
}
// 获取所有已选中列表项关联的桌面小部件属性集合,遍历`mSelectedIndex` HashMap中已选中的项通过游标获取对应的笔记数据
// 再从中提取小部件的ID和类型信息封装成`AppWidgetAttribute`对象添加到`HashSet`集合中返回,方便后续对相关小部件进行更新等操作。
// 注意这里不会关闭游标,因为游标通常由适配器统一管理关闭。
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) {
if (c!= null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
@ -128,6 +134,8 @@ public class NotesListAdapter extends CursorAdapter {
return itemSet;
}
// 获取已选中列表项的数量,通过遍历`mSelectedIndex` HashMap中的值布尔类型表示是否选中统计值为`true`的数量并返回,
// 可用于在界面上展示已选中的项数等相关提示信息。
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
@ -143,11 +151,15 @@ public class NotesListAdapter extends CursorAdapter {
return count;
}
// 判断是否所有符合条件(通常是普通笔记项)的列表项都已被选中,通过比较已选中的项数和总的笔记数量(`mNotesCount`)来判断,
// 如果已选中项数不为0且等于笔记数量则表示全选状态返回`true`,否则返回`false`。
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
return (checkedCount!= 0 && checkedCount == mNotesCount);
}
// 判断指定位置的列表项是否被选中,通过从`mSelectedIndex` HashMap中获取对应位置的选中状态值来判断如果值为`null`则默认未选中,返回`false`
// 否则返回对应的值(`true`表示选中,`false`表示未选中)。
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
@ -155,23 +167,29 @@ public class NotesListAdapter extends CursorAdapter {
return mSelectedIndex.get(position);
}
// 当内容发生变化时(例如数据库数据更新等情况)调用的方法,先调用父类的`onContentChanged`方法,然后重新计算笔记的数量(通过`calcNotesCount`方法),
// 确保相关数据统计与实际数据保持一致,以便后续操作基于正确的数量进行判断等。
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount();
}
// 当更改游标(通常意味着数据更新,更换了新的查询结果游标等情况)时调用的方法,同样先调用父类的`changeCursor`方法,然后重新计算笔记数量,
// 保证数据的一致性以及相关功能基于最新的数据状态运行。
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount();
}
// 私有方法,用于计算笔记的数量,遍历游标中的所有数据项,根据笔记类型判断是否为普通笔记(`Notes.TYPE_NOTE`若是则数量加1
// 最终得到符合条件的笔记数量并更新`mNotesCount`成员变量的值,为其他依赖笔记数量的功能提供准确的数据基础。
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c != null) {
if (c!= null) {
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
@ -181,4 +199,4 @@ public class NotesListAdapter extends CursorAdapter {
}
}
}
}
}

@ -1,19 +1,3 @@
/*
* 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;
@ -29,15 +13,24 @@ 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控件通常以相对时间格式如“5分钟前”“昨天”等展示方便用户直观了解笔记的更新情况。
private TextView mTime;
// 用于显示与笔记相关的联系人姓名的TextView控件如果笔记是通话记录相关的话会显示对应的联系人名字。
private TextView mCallName;
// 存储当前列表项对应的笔记数据对象,方便在视图中获取和展示笔记的各项详细信息。
private NoteItemData mItemData;
// 用于多选模式下显示选中状态的CheckBox控件在多选操作时显示出来供用户勾选选择笔记项。
private CheckBox mCheckBox;
// 构造函数,调用父类构造函数初始化视图,并通过`inflate`方法将布局文件(`R.layout.note_item`)加载到该视图中,
// 同时获取布局文件中定义的各个子控件的实例,方便后续设置其显示内容和样式等操作。
public NotesListItem(Context context) {
super(context);
inflate(context, R.layout.note_item, this);
@ -48,7 +41,11 @@ public class NotesListItem extends LinearLayout {
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
// 绑定数据到该列表项视图上,根据传入的笔记数据(`NoteItemData`)以及当前是否处于多选模式(`choiceMode`)和该项是否被选中(`checked`)等信息,
// 设置各个子控件的显示内容、样式以及可见性等,展示对应笔记的相关信息,并根据笔记的类型、状态等设置合适的背景样式。
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
// 如果处于多选模式且当前笔记类型是普通笔记(`Notes.TYPE_NOTE`则显示CheckBox控件并设置其选中状态与传入的`checked`参数一致;
// 否则隐藏CheckBox控件表示该笔记项在非多选模式或者不是普通笔记类型时不需要展示选中状态。
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
@ -57,17 +54,23 @@ public class NotesListItem extends LinearLayout {
}
mItemData = data;
// 如果笔记的ID是通话记录文件夹的特定ID表示这是通话记录文件夹对应的列表项进行如下特殊的显示设置
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
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()));
// 设置提醒图标为通话记录相关的特定图标通过资源ID `R.drawable.call_record`指定)。
mAlert.setImageResource(R.drawable.call_record);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
}
// 如果笔记所属的父文件夹ID是通话记录文件夹的特定ID表示这是通话记录文件夹下的普通笔记对应的列表项进行如下显示设置
else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
@ -75,10 +78,12 @@ public class NotesListItem extends LinearLayout {
} else {
mAlert.setVisibility(View.GONE);
}
} else {
}
// 如果是其他普通情况(既不是通话记录文件夹本身,也不是其下的笔记),进行如下显示设置:
else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
// 如果笔记类型是文件夹类型,设置标题文本内容为文件夹名称加上该文件夹下的笔记数量信息,展示文件夹相关的综合信息。
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
@ -94,11 +99,15 @@ public class NotesListItem extends LinearLayout {
}
}
}
// 设置笔记修改时间的显示文本,通过`DateUtils.getRelativeTimeSpanString`方法将时间戳转换为相对时间格式的字符串进行展示。
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 根据笔记的数据信息(如类型、状态等)调用`setBackground`方法设置该列表项的背景样式,增强视觉区分效果。
setBackground(data);
}
// 私有方法,根据笔记数据(`NoteItemData`)的类型以及在列表中的位置等状态信息,设置该列表项对应的背景资源,
// 例如普通笔记根据是单个、首个、最后一个或者在文件夹下等不同情况设置不同的背景样式,文件夹则统一设置对应的文件夹背景样式。
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
@ -116,7 +125,8 @@ public class NotesListItem extends LinearLayout {
}
}
// 获取该列表项对应的笔记数据对象的方法,外部可通过调用该方法获取当前列表项所展示笔记的详细数据信息,方便进行其他相关操作。
public NoteItemData getItemData() {
return mItemData;
}
}
}

@ -1,19 +1,3 @@
/*
* 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;
@ -47,35 +31,42 @@ 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";
// 用于存储同步账户名称的偏好设置键值常量字符串通过该键在SharedPreferences中存取同步账户的名称信息。
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;
// 用于接收GTask同步服务广播的广播接收器对象通过接收广播来实时更新偏好设置界面的显示状态比如同步进度等信息。
private GTaskReceiver mReceiver;
// 用于存储原始账户列表的数组,方便在比较账户变化等操作中使用,例如判断是否新增了账户等情况。
private Account[] mOriAccounts;
// 用于标记是否添加了新账户的布尔值,在一些涉及账户变更后需要进行相应处理的逻辑中起判断作用。
private boolean mHasAddedAccount;
// Activity创建时调用的方法进行一些初始化操作如设置ActionBar的返回按钮显示、加载偏好设置资源文件、
// 获取账户分类对象、注册广播接收器以及添加自定义的列表头视图等操作。
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* using the app icon for navigation */
/* 使用应用图标作为导航返回按钮 */
getActionBar().setDisplayHomeAsUpEnabled(true);
// 从资源文件(`R.xml.preferences`)中加载偏好设置的布局和配置信息,显示在界面上。
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
mReceiver = new GTaskReceiver();
@ -88,15 +79,16 @@ public class NotesPreferenceActivity extends PreferenceActivity {
getListView().addHeaderView(header, null, true);
}
// Activity重新恢复到前台时调用的方法主要用于在账户有新增情况时自动设置同步账户并刷新整个偏好设置界面的显示状态
// 通过调用`refreshUI`方法重新加载和更新各个设置项的显示内容及状态。
@Override
protected void onResume() {
super.onResume();
// need to set sync account automatically if user has added a new
// account
// 如果有新添加的账户,需要自动设置同步账户(根据一定的逻辑判断并选择合适的账户进行设置)。
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
if (mOriAccounts!= null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
@ -116,14 +108,17 @@ public class NotesPreferenceActivity extends PreferenceActivity {
refreshUI();
}
// Activity销毁时调用的方法用于注销之前注册的广播接收器避免内存泄漏等问题然后再调用父类的`onDestroy`方法进行后续销毁操作。
@Override
protected void onDestroy() {
if (mReceiver != null) {
if (mReceiver!= null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
// 加载账户偏好设置相关的显示内容和操作逻辑,先移除账户分类下已有的所有设置项,然后创建一个新的账户偏好设置项,
// 设置其标题、摘要以及点击事件监听器,点击时根据不同情况(如是否正在同步、是否已设置过账户等)弹出相应的对话框进行操作提示或处理。
private void loadAccountPreference() {
mAccountCategory.removeAll();
@ -135,17 +130,16 @@ public class NotesPreferenceActivity extends PreferenceActivity {
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// the first time to set account
// 首次设置账户时,弹出选择账户的对话框。
showSelectAccountAlertDialog();
} else {
// if the account has already been set, we need to promp
// user about the risk
// 如果已经设置过账户,弹出确认更改账户的对话框,提示相关风险等信息。
showChangeAccountConfirmAlertDialog();
}
} else {
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
.show();
}
return true;
}
@ -154,11 +148,14 @@ public class NotesPreferenceActivity extends PreferenceActivity {
mAccountCategory.addPreference(accountPref);
}
// 加载同步按钮相关的显示内容和操作逻辑,根据同步服务的状态(是否正在同步)来设置同步按钮的文本内容(如“立即同步”或“取消同步”)、
// 点击事件监听器以及按钮的启用状态(根据是否设置了同步账户来决定是否可用),同时还设置上次同步时间的显示文本和可见性状态,
// 如果正在同步则显示同步进度相关文本,否则根据上次同步时间的有无来显示相应的格式化时间文本或隐藏该视图。
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// set button state
// 设置同步按钮的状态(文本、点击事件和启用状态)。
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
@ -176,13 +173,13 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// set last sync time
// 设置上次同步时间的显示状态(文本和可见性)。
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
if (lastSyncTime!= 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
@ -193,11 +190,14 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}
// 刷新整个偏好设置界面的显示内容和状态,通过调用`loadAccountPreference`和`loadSyncButton`方法分别更新账户相关设置和同步相关设置的显示情况。
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
// 弹出选择账户的警告对话框,用于让用户选择要设置的同步账户,对话框中展示已有谷歌账户列表供用户选择,
// 同时提供添加新账户的入口,选择账户后设置同步账户并刷新界面,添加账户则跳转到添加账户的系统设置页面。
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
@ -254,6 +254,8 @@ public class NotesPreferenceActivity extends PreferenceActivity {
});
}
// 弹出确认更改账户的警告对话框,用于提示用户更改账户的相关风险,并提供更改账户、移除账户和取消操作的菜单项供用户选择,
// 根据用户选择执行相应的逻辑,如调用`showSelectAccountAlertDialog`方法重新选择账户或者调用`removeSyncAccount`方法移除当前账户等操作。
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
@ -283,26 +285,29 @@ public class NotesPreferenceActivity extends PreferenceActivity {
dialogBuilder.show();
}
// 获取设备上已登录的谷歌账户列表,通过`AccountManager`获取类型为“com.google”的账户数组用于在选择账户等相关操作中展示给用户。
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
// 设置同步账户的方法将指定的账户名称保存到SharedPreferences中作为同步账户同时清理上次同步时间以及本地与GTask相关的信息如清空相关字段数据
// 并根据设置情况弹出相应的提示Toast信息告知用户操作结果。
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
if (account!= null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
// 清理上次同步时间将其设置为0表示未同步过。
setLastSyncTime(this, 0);
// clean up local gtask related info
// 清理本地GTask相关信息通过更新数据库中相关字段如`NoteColumns.GTASK_ID`和`NoteColumns.SYNC_ID`)为空值或初始值来实现。
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
@ -318,6 +323,8 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}
// 移除同步账户的方法从SharedPreferences中移除同步账户名称以及上次同步时间相关的键值对同时清理本地GTask相关信息
// 操作完成后界面会通过后续的刷新逻辑(如`refreshUI`方法)更新显示状态。
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
@ -329,7 +336,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
editor.commit();
// clean up local gtask related info
// 清理本地GTask相关信息与`setSyncAccount`方法中类似,通过更新数据库字段来清除相关数据。
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
@ -340,49 +347,11 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}).start();
}
// 静态方法用于从SharedPreferences中获取当前设置的同步账户名称若不存在则返回空字符串方便在其他类中获取该信息进行相关判断或操作。
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
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();
}
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
private class GTaskReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
return false;
}
}
}
// 静态方法用于设置上次同步时间到SharedPreferences中将指定的时间戳保存起来方便后续在界面上展示等操作中获取使用。
Loading…
Cancel
Save