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

262 lines
10 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-2011MiCode 开源社区 (www.micode.net)
* 根据 Apache 许可证 2.0 版本("许可证")授权;
* 除非符合许可证的规定,否则不得使用本文件。
* 您可以从以下网址获取许可证副本:
* http://www.apache.org/licenses/LICENSE-2.0
* 除非适用法律要求或书面同意,本软件按"原样"分发,
* 没有任何明示或暗示的保证或条件。
* 详见许可证中规定的权限和限制。
* 这是一份标准的Apache许可证2.0版本的开源声明)
*/
// 定义当前类的包路径指明该类属于net.micode.notes.ui 包
package net.micode.notes.ui;
/* 导入Android基础类 */
// Context类提供应用环境信息访问接口
import android.content.Context;
// Rect类表示一个矩形区域用于处理视图边界
import android.graphics.Rect;
// Layout类用于管理文本布局和测量
import android.text.Layout;
// Selection类用于处理文本选择操作
import android.text.Selection;
// Spanned接口表示带有样式的文本
import android.text.Spanned;
// TextUtils类提供文本处理工具方法
import android.text.TextUtils;
// URLSpan类用于标记文本中的超链接
import android.text.style.URLSpan;
// AttributeSet接口表示XML属性集合
import android.util.AttributeSet;
// Log类提供日志记录功能
import android.util.Log;
/* 导入Android视图相关类 */
// ContextMenu类表示上下文菜单
import android.view.ContextMenu;
// KeyEvent类表示按键事件
import android.view.KeyEvent;
// MenuItem类表示菜单项
import android.view.MenuItem;
// OnMenuItemClickListener接口定义菜单项点击回调
import android.view.MenuItem.OnMenuItemClickListener;
// MotionEvent类表示触摸事件
import android.view.MotionEvent;
// EditText类是基础文本编辑框
import android.widget.EditText;
/* 导入项目资源 */
// 导入项目R类用于访问资源ID
import net.micode.notes.R;
/* 导入Java集合类 */
// HashMap类实现哈希表结构的Map接口
import java.util.HashMap;
// Map接口表示键值对映射集合
import java.util.Map;
// 继承自AppCompatEditText的支持兼容的编辑文本框
public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText {
private static final String TAG = "NoteEditText"; // 日志标签
private int mIndex; // 当前编辑框在列表中的索引
private int mSelectionStartBeforeDelete; // 删除操作前的选择开始位置
// 定义支持的URL协议常量
private static final String SCHEME_TEL = "tel:";
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 {
// 初始化协议对应的字符串资源
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话协议
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页协议
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);// 邮件协议
}
/**
* 定义文本变化监听接口由NoteEditActivity实现
* 用于处理编辑文本框的删除和新增事件
*/
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; // 文本变化监听器实例
// 第一个构造方法
public NoteEditText(Context context) {
super(context, null);
mIndex = 0; // 默认索引为0
}
// 设置当前编辑框索引
public void setIndex(int index) {
mIndex = index;
}
// 设置文本变化监听器
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 第二个构造方法
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);
// TODO: 待实现的构造函数
}
// 触摸事件处理
@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); // 调用父类处理方法
}
// 按键按下事件处理
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER: // 回车键
if (mOnTextViewChangeListener != null) {
return false; // 交给onKeyUp处理
}
break;
case KeyEvent.KEYCODE_DEL: // 删除键
mSelectionStartBeforeDelete = getSelectionStart(); // 记录删除前的位置
break;
default:
break;
}
return super.onKeyDown(keyCode, event); // 调用父类处理方法
}
// 按键释放事件处理
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL: // 删除键
if (mOnTextViewChangeListener != null) {
// 如果在起始位置删除且不是第一个编辑框,则触发删除回调
if (0 == mSelectionStartBeforeDelete && mIndex != 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); // 调用父类处理方法
}
// 焦点变化处理
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
// 通知监听器文本变化状态
if (mOnTextViewChangeListener != null) {
mOnTextViewChangeListener.onTextChange(mIndex, focused || !TextUtils.isEmpty(getText()));
}
super.onFocusChanged(focused, direction, previouslyFocusedRect); // 调用父类方法
}
// 创建上下文菜单
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() != null) {
// 获取当前选择的起止位置
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) { // 如果选中了一个URL
int defaultResId = 0;
// 根据URL协议类型获取对应的资源ID
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().contains(schema)) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
// 如果没有匹配的协议,使用默认资源
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 添加上下文菜单项并设置点击监听器
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 点击时执行URL跳转
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu); // 调用父类方法
}
}