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.

331 lines
11 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
* 描述: 便签编辑文本框控件
* 作用: 提供便签编辑的核心文本输入组件,支持特殊键处理、链接识别和上下文菜单
* 功能:
* 1. 处理回车键和删除键的特殊行为
* 2. 支持文本变化监听和回调
* 3. 支持URL链接的识别和处理
* 4. 提供自定义的触摸事件处理
*/
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为便签应用提供自定义的文本编辑功能
* 处理特殊按键事件(如回车键、删除键)以支持便签的编辑操作
* 实现URL链接的识别和处理支持点击链接执行相应操作
*/
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:" ;
/** 链接类型与对应菜单文本资源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;
/**
* 构造函数
*
* @param context 上下文环境
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
/**
* 设置当前编辑框在列表中的索引位置
*
* @param index 索引位置
*/
public void setIndex(int index) {
mIndex = index;
}
/**
* 设置文本视图变化监听器
*
* @param listener 监听器实例
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
* 构造函数
*
* @param context 上下文环境
* @param attrs 属性集
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
* 构造函数
*
* @param context 上下文环境
* @param attrs 属性集
* @param defStyle 默认样式
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
/**
* 处理触摸事件
* 实现精确的光标定位,使用户可以点击文本的任意位置设置光标
*
* @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();
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 是否已处理事件
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 如果设置了监听器回车键由Activity处理
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) {
// 当光标在文本开头且不是第一个编辑框时,删除当前编辑框
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);
}
/**
* 创建上下文菜单
* 为识别到的URL链接提供操作选项
*
* @param menu 上下文菜单
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
// 检查文本中是否包含URL链接
if (getText() instanceof Spanned) {
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取选中区域内的所有URL链接
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
// 根据链接类型确定菜单文本
int defaultResId = 0;
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);
}
}