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.
git.text/src/ui/NoteEditText.java

288 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;
/**
* 小米便签的自定义文本输入控件继承自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:" ; // 电话协议(如"tel:10086"
private static final String SCHEME_HTTP = "http:" ; // 网页协议(如"http://www.micode.net"
private static final String SCHEME_EMAIL = "mailto:" ; // 邮件协议(如"mailto:test@micode.net"
// 链接协议与对应菜单资源的映射(用于上下文菜单显示对应操作文字)
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); // 邮件链接对应"发送邮件"菜单
}
/**
* 文本视图变化监听器,由{@link 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);
}
// 文本变化监听器实例由NoteEditActivity设置
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
* 构造方法:初始化文本输入控件(仅上下文参数)
* @param context 上下文环境如NoteEditActivity
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0; // 默认索引为0
}
/**
* 设置当前输入框在多输入框列表中的索引
* @param index 输入框索引
*/
public void setIndex(int index) {
mIndex = index;
}
/**
* 设置文本变化监听器关联到NoteEditActivity
* @param listener 实现了OnTextViewChangeListener的监听器
*/
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 按键代码如KEYCODE_DEL、KEYCODE_ENTER
* @param event 键盘事件
* @return 事件是否被处理
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 回车键事件由onKeyUp处理此处返回false让事件继续传递
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);
}
/**
* 创建上下文菜单,识别选中的链接并显示对应操作选项(如拨打、打开网页)
* @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;
// 识别链接协议,匹配对应的菜单资源
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); // 触发URLSpan的点击事件
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
}