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.
git1/src/java/net/micode/notes/ui/NoteEditText.java

457 lines
17 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.
*/
// NoteEditText.java - 自定义便签编辑文本控件
// 主要功能扩展EditText支持清单模式的特殊按键处理、超链接识别和上下文菜单
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础
import android.content.Context; // 上下文
import android.content.ContentResolver; // 内容解析器
import android.graphics.Rect; // 矩形区域
// Android文本处理
import android.text.Layout; // 文本布局
import android.text.Selection; // 文本选择
import android.text.Spanned; // 可设置样式的文本
import android.text.TextUtils; // 文本工具
import android.text.style.URLSpan; // URL超链接样式
import android.util.AttributeSet; // 属性集
import android.util.Log; // 日志工具
// Android菜单
import android.view.ContextMenu; // 上下文菜单
import android.view.KeyEvent; // 按键事件
import android.view.MenuItem; // 菜单项
import android.view.MenuItem.OnMenuItemClickListener; // 菜单项点击监听
import android.view.MotionEvent; // 触摸事件
// Android文本处理
import android.text.InputType; // 输入类型
import android.text.Spannable; // 可设置样式的文本
import android.text.SpannableStringBuilder; // 可设置样式的字符串构建器
import android.text.style.ImageSpan; // 图片样式
// Android输入法
import android.view.inputmethod.EditorInfo; // 输入法编辑器信息
// Android控件
import android.widget.EditText; // 编辑文本控件基类
import android.widget.ImageView; // 图片视图
// Android图形
import android.graphics.Bitmap; // 位图
import android.graphics.BitmapFactory; // 位图工厂
import android.graphics.drawable.Drawable; // 可绘制对象
// Android网络
import android.net.Uri; // URI工具
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// Java集合
import java.util.HashMap; // 哈希映射
import java.util.Map; // 映射接口
// ======================= 便签编辑文本控件 =======================
/**
* NoteEditText - 自定义便签编辑文本控件
* 继承自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:"; // 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); // 邮件链接
}
// ======================= 文本变化监听器接口 =======================
/**
* OnTextViewChangeListener - 文本视图变化监听器接口
* 由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;
// ======================= 构造函数 =======================
/**
* 构造函数1 - 简化版本
* @param context 上下文
*/
public NoteEditText(Context context) {
this(context, null);
mIndex = 0; // 默认索引为0
}
/**
* 构造函数2 - 完整属性
* @param context 上下文
* @param attrs 属性集
*/
public NoteEditText(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.editTextStyle);
}
/**
* 构造函数3 - 包含样式
* @param context 上下文
* @param attrs 属性集
* @param defStyle 默认样式
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 确保输入法支持中文输入
setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
setImeOptions(EditorInfo.IME_ACTION_NONE);
}
/**
* 从URI加载图片
* @param uri 图片URI
* @return 加载的Bitmap对象
*/
private Bitmap loadImageFromUri(Uri uri) {
try {
ContentResolver resolver = getContext().getContentResolver();
Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
if (bitmap != null) {
// 调整图片大小以适应编辑框
int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
if (bitmap.getWidth() > maxWidth) {
float scale = (float) maxWidth / bitmap.getWidth();
int newHeight = (int) (bitmap.getHeight() * scale);
bitmap = Bitmap.createScaledBitmap(bitmap, maxWidth, newHeight, true);
}
}
return bitmap;
} catch (Exception e) {
Log.e(TAG, "Failed to load image from URI: " + uri, e);
return null;
}
}
/**
* 解析文本中的[IMAGE:uri]标签并替换为图片
* @param text 包含图片标签的文本
* @return 处理后的SpannableStringBuilder包含图片
*/
private SpannableStringBuilder parseImageTags(CharSequence text) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
String content = text.toString();
int startIndex = 0;
while (true) {
startIndex = content.indexOf("[IMAGE:", startIndex);
if (startIndex == -1) break;
int endIndex = content.indexOf("]", startIndex);
if (endIndex == -1) break;
String imageTag = content.substring(startIndex, endIndex + 1);
String imageUriStr = imageTag.substring(7, imageTag.length() - 1); // 去掉[IMAGE:和]
try {
Uri imageUri = Uri.parse(imageUriStr);
Bitmap bitmap = loadImageFromUri(imageUri);
if (bitmap != null) {
// 创建一个占位符文本,用于放置图片
String placeholder = "[图片]";
int placeholderLength = placeholder.length();
// 替换图片标签为占位符
builder.replace(startIndex, endIndex + 1, placeholder);
// 创建ImageSpan并添加到占位符位置
ImageSpan imageSpan = new ImageSpan(getContext(), bitmap);
builder.setSpan(imageSpan, startIndex, startIndex + placeholderLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// 更新content和startIndex以继续处理
content = builder.toString();
startIndex += placeholderLength;
} else {
startIndex = endIndex + 1;
}
} catch (Exception e) {
Log.e(TAG, "Failed to parse image URI: " + imageUriStr, e);
startIndex = endIndex + 1;
}
}
return builder;
}
/**
* 重写setText方法解析图片标签并显示图片
*/
@Override
public void setText(CharSequence text, BufferType type) {
if (text != null) {
SpannableStringBuilder builder = parseImageTags(text);
super.setText(builder, BufferType.SPANNABLE);
} else {
super.setText(text, type);
}
}
// ======================= 设置方法 =======================
/**
* 设置项索引
* 在清单模式中标识当前是第几个列表项
* @param index 索引值
*/
public void setIndex(int index) {
mIndex = index;
}
/**
* 设置文本变化监听器
* @param listener 监听器实例
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// ======================= 触摸事件处理 =======================
/**
* 触摸事件处理
* 重写以实现精确的触摸位置选择
* @param event 触摸事件
* @return true: 已处理; false: 未处理
*/
@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();
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 true: 已处理; false: 未处理
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 拦截回车键在onKeyUp中处理
if (mOnTextViewChangeListener != null) {
return false; // 返回false让系统继续处理
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录删除前的选择位置
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
return super.onKeyDown(keyCode, event);
}
// ======================= 按键抬起事件 =======================
/**
* 按键抬起事件处理
* 处理清单模式的特殊按键逻辑
* @param keyCode 按键代码
* @param event 按键事件
* @return true: 已处理; false: 未处理
*/
@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);
}
// ======================= 焦点变化处理 =======================
/**
* 焦点变化处理
* 当焦点变化时通知监听器文本状态
* @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);
// 获取选择范围内的超链接
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
// 只处理单个超链接的情况
if (urls.length == 1) {
int defaultResId = 0;
// 根据URL协议确定菜单文本
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;
}
// 添加菜单项
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 触发超链接点击
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
}