You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.widget.EditText;
import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
/**
* NoteEditText 类继承自 EditText用于处理笔记编辑过程中的一些特殊逻辑
* 如处理删除、回车按键事件,处理链接点击事件等。
*/
public class NoteEditText extends EditText {
// 日志标签,用于在日志中标识该类的信息
private static final String TAG = "NoteEditText";
// 该编辑文本框的索引,用于在列表中定位
private int mIndex;
// 删除操作前的选择起始位置
private int mSelectionStartBeforeDelete;
// 电话链接的协议前缀
private static final String SCHEME_TEL = "tel:" ;
// HTTP 链接的协议前缀
private static final String SCHEME_HTTP = "http:" ;
// 邮件链接的协议前缀
private static final String SCHEME_EMAIL = "mailto:" ;
// 存储协议前缀和对应的菜单项资源 ID 的映射
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
// 初始化映射,将电话协议前缀映射到对应的菜单项资源 ID
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
// 初始化映射,将 HTTP 协议前缀映射到对应的菜单项资源 ID
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
// 初始化映射,将邮件协议前缀映射到对应的菜单项资源 ID
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
* OnTextViewChangeListener 接口定义了在编辑文本框发生特定事件时的回调方法,
* 由外部类实现,用于处理删除、回车和文本变化等事件。
*/
public interface OnTextViewChangeListener {
/**
* 当按下删除键且文本为空时,调用此方法删除当前编辑文本框。
* @param index 当前编辑文本框的索引
* @param text 当前编辑文本框的文本内容
*/
void onEditTextDelete(int index, String text);
/**
* 当按下回车键时,调用此方法在当前编辑文本框之后添加一个新的编辑文本框。
* @param index 当前编辑文本框的下一个索引
* @param text 当前编辑文本框中光标之后的文本内容
*/
void onEditTextEnter(int index, String text);
/**
* 当文本内容发生变化时,调用此方法隐藏或显示菜单项。
* @param index 当前编辑文本框的索引
* @param hasText 当前编辑文本框是否有文本内容
*/
void onTextChange(int index, boolean hasText);
}
// 文本变化监听器实例
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
* 构造函数,使用默认属性创建 NoteEditText 实例。
* @param context 上下文对象
*/
public NoteEditText(Context context) {
super(context, null);
// 初始化索引为 0
mIndex = 0;
}
/**
* 设置当前编辑文本框的索引。
* @param index 要设置的索引值
*/
public void setIndex(int index) {
mIndex = index;
}
/**
* 设置文本变化监听器。
* @param listener 实现了 OnTextViewChangeListener 接口的监听器实例
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
* 构造函数,使用指定的属性集创建 NoteEditText 实例。
* @param context 上下文对象
* @param attrs 属性集
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
* 构造函数,使用指定的属性集和默认样式创建 NoteEditText 实例。
* @param context 上下文对象
* @param attrs 属性集
* @param defStyle 默认样式
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
/**
* 处理触摸事件,当手指按下时,根据触摸位置设置光标位置。
* @param event 触摸事件对象
* @return 是否处理该事件
*/
@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 += getScrollX();
// 加上垂直滚动距离
y += getScrollY();
// 获取文本布局对象
Layout layout = getLayout();
// 根据垂直位置获取所在行
int line = layout.getLineForVertical(y);
// 根据行和水平位置获取偏移量
int off = layout.getOffsetForHorizontal(line, x);
// 设置光标位置
Selection.setSelection(getText(), off);
break;
}
return super.onTouchEvent(event);
}
/**
* 处理按键按下事件,记录删除操作前的选择起始位置,处理回车键事件。
* @param keyCode 按键码
* @param event 按键事件对象
* @return 是否处理该事件
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
// 如果有监听器,不处理回车键事件,交由监听器处理
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录删除操作前的选择起始位置
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
return super.onKeyDown(keyCode, event);
}
/**
* 处理按键释放事件,根据按键码调用相应的监听器方法。
* @param keyCode 按键码
* @param event 按键事件对象
* @return 是否处理该事件
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener != null) {
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
// 当删除操作从文本开头开始且索引不为 0 时,调用删除回调方法
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
// 记录日志,提示监听器未设置
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
// 获取当前选择的起始位置
int selectionStart = getSelectionStart();
// 获取光标之后的文本内容
String text = getText().subSequence(selectionStart, length()).toString();
// 设置文本为光标之前的内容
setText(getText().subSequence(0, selectionStart));
// 调用回车回调方法
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
// 记录日志,提示监听器未设置
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
return super.onKeyUp(keyCode, event);
}
/**
* 处理焦点变化事件,根据焦点状态和文本内容调用文本变化监听器方法。
* @param focused 是否获得焦点
* @param direction 焦点变化的方向
* @param previouslyFocusedRect 之前焦点所在的矩形区域
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
if (!focused && TextUtils.isEmpty(getText())) {
// 失去焦点且文本为空时,调用文本变化回调方法,标记为无文本
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
// 其他情况,调用文本变化回调方法,标记为有文本
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
* 创建上下文菜单,当选中的文本包含链接时,添加相应的菜单项。
* @param menu 上下文菜单对象
*/
@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) {
// 如果只有一个 URLSpan 对象
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
// 根据链接协议前缀获取对应的菜单项资源 ID
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
if (defaultResId == 0) {
// 如果没有匹配的协议前缀,使用默认的菜单项资源 ID
defaultResId = R.string.note_link_other;
}
// 添加菜单项,并设置点击监听器
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 点击菜单项时,触发链接的点击事件
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
}