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

294 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;
/**
* 自定义编辑文本框,用于笔记应用的清单模式
* 支持特殊的删除和回车逻辑,以及链接处理功能
*/
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>();
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 新编辑框的索引(当前索引+1
* @param text 从当前光标位置截取的文本(将移动到新编辑框)
*/
void onEditTextEnter(int index, String text);
/**
* 当文本内容变化时,显示或隐藏选项(如复选框)
* @param index 发生变化的编辑框索引
* @param hasText 变化后是否有文本
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
* 构造函数1仅通过Context创建
*/
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;
}
/**
* 构造函数2通过Context和AttributeSet创建XML布局使用
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
* 构造函数3通过Context、AttributeSet和样式定义创建
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
/**
* 触摸事件处理 - 用于精确的链接点击定位
* 当手指按下时,计算点击位置对应的字符偏移量,并设置光标到那里
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 手指按下动作
// 计算点击处在文本内容中的准确坐标
int x = (int) event.getX();
int y = (int) event.getY();
// 减去内边距,得到文本区域内的坐标
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
// 加上滚动偏移量,得到文本内容中的绝对坐标
x += getScrollX();
y += getScrollY();
// 获取文本布局对象
Layout layout = getLayout();
// 根据y坐标找到对应的行号
int line = layout.getLineForVertical(y);
// 根据x坐标在该行找到对应的字符偏移量
int off = layout.getOffsetForHorizontal(line, x);
// 将光标设置到计算出的偏移量位置
Selection.setSelection(getText(), off);
break;
}
// 调用父类方法处理其他触摸逻辑(如长按弹出菜单)
return super.onTouchEvent(event);
}
/**
* 按键按下事件处理
* 主要记录删除键按下时光标的位置
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER: // 回车键
if (mOnTextViewChangeListener != null) {
// 如果监听器存在返回false让onKeyUp处理
return false;
}
break;
case KeyEvent.KEYCODE_DEL: // 删除键(退格键)
// 记录删除前光标的起始位置用于在onKeyUp中判断是否在行首
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) {
// 判断条件光标在行首位置0且不是第一个编辑框索引不为0
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
// 触发删除监听通知Activity删除当前编辑框
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));
// 触发回车监听通知Activity插入新编辑框并传递截取的文本
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) {
// 如果失去焦点且文本为空,通知无文本状态
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
// 其他情况(获得焦点,或失去焦点但文本不为空),通知有文本状态
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
* 创建上下文菜单(长按编辑框时弹出的菜单)
* 特别处理文本中的链接,为链接添加对应的操作菜单项
*/
@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);
// 从文本中获取选区范围内的所有链接
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
// 如果选区内有且仅有一个链接
if (urls.length == 1) {
int defaultResId = 0; // 菜单项文本的资源ID
// 遍历协议映射检查链接的URL包含哪种协议
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
// 找到匹配的协议获取对应的字符串资源ID
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) {
// 触发链接的默认行为(如打开浏览器、拨号盘等)
urls[0].onClick(NoteEditText.this);
return true; // 消费此事件
}
});
}
}
// 调用父类方法添加系统默认的菜单项(复制、粘贴等)
super.onCreateContextMenu(menu);
}
}