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

270 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.

/*
* 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";
// 当前 NoteEditText 的索引,用于区分不同的 EditText。
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);
}
/**
* 用于监听 NoteEditText 变化的接口。可以监听删除、回车和文本变化。
*/
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);
}
// 存储 OnTextViewChangeListener 的实例
private OnTextViewChangeListener mOnTextViewChangeListener;
// 构造函数,初始化 NoteEditText。
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
// 设置当前 NoteEditText 的索引
public void setIndex(int index) {
mIndex = index;
}
// 设置监听文本变化的监听器
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 通过属性集初始化 NoteEditText
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
// 通过属性集和默认样式初始化 NoteEditText
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 处理触摸事件,当点击文本时更新光标的位置。
* @param event 触摸事件
* @return 是否处理该事件
*/
@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(); // 获取点击点的 x 坐标
y -= getTotalPaddingTop(); // 获取点击点的 y 坐标
x += getScrollX(); // 获取滚动的 x 偏移
y += getScrollY(); // 获取滚动的 y 偏移
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) {
// 如果删除键按下时光标位置在起始位置并且索引不为 0则通知监听器删除操作
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not setted");
}
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 setted");
}
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);
}
/**
* 创建上下文菜单,当文本中包含 URL 链接时,显示相应的菜单项。
* @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) {
int defaultResId = 0;
// 根据 URL 的 Scheme 映射到相应的菜单项
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
if (defaultResId == 0) {
defaultResId = R.string.note_link_other; // 如果没有匹配的 Scheme则使用默认菜单项
}
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 当点击菜单项时,执行 URLSpan 的点击事件
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
}