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.
MiNote/ui/NoteEditText.java

245 lines
8.9 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.

/*
* 版权所有 (c) 2010-2011The MiCode Open Source Community (www.micode.net)
*
* 本软件根据 Apache 许可证 2.0 版("许可证")发布;
* 除非符合许可证,否则不得使用此文件。
* 您可以在以下网址获取许可证副本:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 除非法律要求或书面同意,软件
* 根据许可证分发的内容按"原样"提供,
* 不附带任何明示或暗示的保证或条件。
* 请参阅许可证,了解有关权限和限制的具体语言。
*/
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;
/**
* 自定义笔记编辑文本组件继承自EditText
* 功能:
* 1. 处理文本输入时的特殊按键事件(删除、换行)
* 2. 支持文本变化监听,通知上层组件更新
* 3. 识别文本中的链接(电话、网址、邮箱),并提供上下文菜单操作
* 4. 优化触摸事件处理,确保光标位置准确
*/
public class NoteEditText extends EditText {
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 Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>() {{
put(SCHEME_TEL, R.string.note_link_tel); // 电话链接
put(SCHEME_HTTP, R.string.note_link_web); // 网页链接
put(SCHEME_EMAIL, R.string.note_link_email); // 邮箱链接
}};
// 文本变化监听器接口
public interface OnTextViewChangeListener {
/**
* 当按下删除键且文本为空时触发(删除当前编辑项)
* @param index 当前编辑项索引
* @param text 当前文本内容
*/
void onEditTextDelete(int index, String text);
/**
* 当按下回车键时触发(添加新编辑项)
* @param index 当前编辑项索引+1
* @param text 新编辑项的文本内容
*/
void onEditTextEnter(int index, String text);
/**
* 当文本内容变化时触发(更新界面状态)
* @param index 当前编辑项索引
* @param hasText 是否有文本内容
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener; // 监听器实例
public NoteEditText(Context context) {
super(context, null);
mIndex = 0; // 初始化索引为0
}
/**
* 设置编辑项索引(用于标识在列表中的位置)
* @param index 索引值
*/
public void setIndex(int index) {
mIndex = index;
}
/**
* 设置文本变化监听器
* @param listener 监听器实例
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 构造方法支持XML布局初始化
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 触摸事件处理:确保点击位置正确设置光标
* @param event 触摸事件
* @return 事件处理结果
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// 计算触摸点在文本中的坐标
int x = (int) event.getX();
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); // 设置光标位置
}
return super.onTouchEvent(event);
}
/**
* 按键按下事件处理
* @param keyCode 按键码
* @param event 按键事件
* @return 事件处理结果
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 回车键由onKeyUp处理此处返回false以便后续处理
if (mOnTextViewChangeListener != null) return false;
break;
case KeyEvent.KEYCODE_DEL:
// 记录删除前的光标位置
mSelectionStartBeforeDelete = getSelectionStart();
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 (mSelectionStartBeforeDelete == 0 && getText().length() == 0 && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true; // 消费事件,阻止默认删除行为
}
}
break;
case KeyEvent.KEYCODE_ENTER:
// 回车键处理:分割文本并通知添加新编辑项
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart();
String newText = getText().subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart)); // 保留当前行文本
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, newText); // 通知添加新项
return true; // 消费事件,阻止默认换行行为
}
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) {
boolean hasText = !TextUtils.isEmpty(getText());
mOnTextViewChangeListener.onTextChange(mIndex, hasText); // 通知文本是否有内容
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
* 创建上下文菜单:处理链接点击
* @param menu 上下文菜单
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
super.onCreateContextMenu(menu);
Spanned text = (Spanned) getText();
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取选中区域内的URLSpan
URLSpan[] urls = text.getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
String url = urls[0].getURL();
int menuTextResId = R.string.note_link_other; // 默认菜单文本
// 根据链接协议设置对应的菜单文本
for (Map.Entry<String, Integer> entry : sSchemaActionResMap.entrySet()) {
if (url.startsWith(entry.getKey())) {
menuTextResId = entry.getValue();
break;
}
}
// 添加菜单选项:点击时触发链接点击事件
menu.add(0, 0, 0, menuTextResId).setOnMenuItemClickListener(item -> {
urls[0].onClick(NoteEditText.this); // 调用URLSpan的点击处理
return true;
});
}
}
}