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

430 lines
13 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.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.text.style.ForegroundColorSpan;
import android.graphics.Typeface;
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;
/**
* 自定义笔记编辑文本框
* <p>
* 该类继承自EditText用于小米便签应用中的笔记编辑支持链接识别、键盘事件处理
* 和文本视图变化监听等功能
* </p>
*/
public class NoteEditText extends EditText {
/**
* 日志标签
*/
private static final String TAG = "NoteEditText";
/**
* 当前编辑框的索引
*/
private int mIndex;
/**
* 删除操作前的选择起始位置
*/
private int mSelectionStartBeforeDelete;
/**
* 电话链接协议
*/
private static final String SCHEME_TEL = "tel:" ;
/**
* HTTP链接协议
*/
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);
}
/**
* 文本视图变化监听器接口
* <p>
* 由{@link NoteEditActivity}调用,用于处理编辑框的删除或添加
* </p>
*/
public interface OnTextViewChangeListener {
/**
* 当按下{@link KeyEvent#KEYCODE_DEL}且文本为空时,删除当前编辑框
*/
void onEditTextDelete(int index, String text);
/**
* 当按下{@link KeyEvent#KEYCODE_ENTER}时,在当前编辑框后添加新的编辑框
*/
void onEditTextEnter(int index, String text);
/**
* 当文本变化时,隐藏或显示项目选项
*/
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
}
/**
* 处理触摸事件
* <p>
* 当用户点击编辑框时,将光标定位到点击位置
* </p>
* @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);
}
/**
* 处理按键按下事件
* <p>
* 监听回车键和删除键的按下事件
* </p>
* @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);
}
/**
* 处理按键释放事件
* <p>
* 监听删除键和回车键的释放事件,实现编辑框的删除和添加功能
* </p>
* @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);
}
/**
* 处理焦点变化事件
* <p>
* 当编辑框失去焦点且文本为空时,通知监听器隐藏项目选项
* 当编辑框获得焦点或文本不为空时,通知监听器显示项目选项
* </p>
* @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);
}
/**
* 创建上下文菜单
* <p>
* 当选中的文本包含链接时,创建相应的链接操作菜单
* </p>
* @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) {
// goto a new intent
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
/**
* 应用粗体格式到选中文本
*/
public void applyBold() {
applyStyle(Typeface.BOLD);
}
/**
* 应用斜体格式到选中文本
*/
public void applyItalic() {
applyStyle(Typeface.ITALIC);
}
/**
* 应用下划线格式到选中文本
*/
public void applyUnderline() {
SpannableStringBuilder ssb = new SpannableStringBuilder(getText());
int start = getSelectionStart();
int end = getSelectionEnd();
if (start != end) {
// 检查是否已存在下划线样式
UnderlineSpan[] existingUnderlines = ssb.getSpans(start, end, UnderlineSpan.class);
if (existingUnderlines.length > 0) {
// 移除下划线
for (UnderlineSpan span : existingUnderlines) {
ssb.removeSpan(span);
}
} else {
// 添加下划线
ssb.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
}
setText(ssb);
setSelection(start, end);
}
}
/**
* 应用指定颜色到选中文本
* @param color 要应用的颜色值
*/
public void applyTextColor(int color) {
SpannableStringBuilder ssb = new SpannableStringBuilder(getText());
int start = getSelectionStart();
int end = getSelectionEnd();
if (start != end) {
// 移除已有的文字颜色
ForegroundColorSpan[] existingSpans = ssb.getSpans(start, end, ForegroundColorSpan.class);
for (ForegroundColorSpan span : existingSpans) {
ssb.removeSpan(span);
}
// 添加新的文字颜色
ssb.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
setText(ssb);
setSelection(start, end);
}
}
/**
* 应用字体样式到选中文本
*/
private void applyStyle(int style) {
SpannableStringBuilder ssb = new SpannableStringBuilder(getText());
int start = getSelectionStart();
int end = getSelectionEnd();
if (start != end) {
// 获取当前选中文本的所有样式
StyleSpan[] existingSpans = ssb.getSpans(start, end, StyleSpan.class);
// 计算当前选中文本的总样式
int currentStyle = Typeface.NORMAL;
if (existingSpans.length > 0) {
// 如果有样式,取第一个样式(假设所有样式一致)
currentStyle = existingSpans[0].getStyle();
}
// 计算新的字体样式
int newStyle;
if ((currentStyle & style) != 0) {
// 移除该样式
newStyle = currentStyle & ~style;
} else {
// 添加该样式
newStyle = currentStyle | style;
}
// 移除旧的样式
for (StyleSpan span : existingSpans) {
ssb.removeSpan(span);
}
// 添加新的样式
ssb.setSpan(new StyleSpan(newStyle), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
setText(ssb);
setSelection(start, end);
}
}
}