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

297 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.
*/
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;
/**
* 小米笔记的自定义文本编辑组件扩展了Android原生EditText的功能
* 支持特殊的按键处理、链接识别和动态添加/删除编辑框
*/
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:" ; // 邮件协议
// 协议到字符串资源的映射表,用于上下文菜单显示
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;
//新增选区变化回调接口
private OnSelectionChangeListener mOnSelectionChangeListener;
/**
* 构造函数创建NoteEditText实例
* @param context 上下文对象
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
/**
* 设置当前编辑框在容器中的索引位置
* @param index 索引值
*/
public void setIndex(int index) {
mIndex = index;
}
/**
* 设置文本变化监听器用于与NoteEditActivity通信
* @param listener 监听器实例
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
//新增选区变化回调接口设置方法
public void setOnSelectionChangeListener(OnSelectionChangeListener listener) {
mOnSelectionChangeListener = listener;
}
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
* 构造函数从XML布局创建NoteEditText实例支持自定义样式
* @param context 上下文对象
* @param attrs XML属性集合
* @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:
// 回车键按下时交由onKeyUp处理
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));
// 通知Activity添加新的编辑框
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) {
// 失去焦点且文本为空时通知Activity隐藏相关选项
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);
// 查找选中区域内的URLSpan
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);
}
}
// 新增选区变化回调接口
interface OnSelectionChangeListener {
void onSelectionChanged(int index, int selStart, int selEnd);
}