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

269 lines
16 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.
* 总体分析
这段 Java 代码定义了一个名为NoteEditText的自定义EditText类它在继承安卓原生EditText的基础上添加了诸多针对特定业务需求的功能逻辑。主要用于处理文本编辑过程中的一些交互操作比如按键按下与抬起时的逻辑像删除、回车键按下等情况、触摸事件处理、焦点变化处理以及上下文菜单创建等同时定义了一个接口用于和外部代码进行交互通知外部文本编辑相关的状态变化情况整体旨在为笔记编辑等类似场景下的文本输入操作提供更贴合业务需求的交互功能。
函数分析
构造函数相关
NoteEditText(Context context)、NoteEditText(Context context, AttributeSet attrs)、NoteEditText(Context context, AttributeSet attrs, int defStyle)
所属类NoteEditText
功能:
第一个构造函数只接收Context参数调用父类EditText构造函数并传入Context和null同时初始化成员变量mIndex为0用于记录当前文本编辑框的某种索引可能与多个编辑框排序等相关
第二个构造函数接收Context和AttributeSet参数调用父类构造函数并传入Context、AttributeSet以及指定的编辑框样式android.R.attr.editTextStyle用于按照传入的属性集合和默认样式来初始化编辑框。
第三个构造函数接收Context、AttributeSet和defStyle参数调用父类构造函数传入对应参数进行初始化不过其内部暂时没有额外自定义的初始化逻辑代码中 TODO Auto-generated constructor stub 提示后续可能还有完善的地方)。
触摸事件处理函数
onTouchEvent(MotionEvent event)
所属类NoteEditText
功能重写了父类的onTouchEvent方法来处理触摸操作相关逻辑。当触摸事件的动作是ACTION_DOWN手指按下屏幕
首先获取触摸点相对于编辑框内部的坐标通过减去总内边距并加上滚动偏移量等计算然后根据触摸点的垂直坐标获取所在的文本行再根据水平坐标获取该行对应的文本偏移量最后通过Selection.setSelection方法将文本选中位置设置为该偏移量对应的位置实现触摸选中文本的功能使得用户触摸文本区域时能方便地定位和选中相应文本内容符合常见的文本编辑交互习惯。
最后返回调用父类onTouchEvent方法的结果保证其他未处理的触摸相关逻辑能按照原生EditText的行为继续执行。
按键按下处理函数
onKeyDown(int keyCode, KeyEvent event)
所属类NoteEditText
功能重写了父类的onKeyDown方法来处理按键按下的相关逻辑。根据按下的不同按键码keyCode进行不同操作
当按下的是回车键KEYCODE_ENTER如果设置了OnTextViewChangeListener监听器用于和外部代码交互通知文本编辑状态变化的接口则直接返回false这可能是为了后续在onKeyUp方法中统一处理回车键抬起的逻辑或者让外部代码有更多控制权决定回车键按下后的具体行为比如是否添加新的编辑文本等
当按下的是删除键KEYCODE_DEL记录当前文本选中位置的起始点通过getSelectionStart方法获取并赋值给mSelectionStartBeforeDelete变量用于后续在删除键抬起时判断是否满足特定的删除文本逻辑比如是否删除当前编辑框的全部文本等情况。
对于其他按键按下情况则不做额外自定义处理直接执行父类的onKeyDown方法逻辑。
按键抬起处理函数
onKeyUp(int keyCode, KeyEvent event)
所属类NoteEditText
功能重写了父类的onKeyUp方法来处理按键抬起的相关逻辑。根据抬起的不同按键码keyCode进行不同操作
当按键码是删除键KEYCODE_DEL如果设置了OnTextViewChangeListener监听器会进一步判断如果当前文本选中位置起始点为0且当前编辑框的索引mIndex不为0则调用监听器的onEditTextDelete方法并传入当前编辑框索引以及编辑框内的文本内容通过getText方法获取并转换为字符串通知外部代码当前编辑框文本被删除的情况最后返回true表示该按键抬起事件已被处理避免父类重复处理。若没有设置监听器则打印日志提示监听器未设置。
当按键码是回车键KEYCODE_ENTER如果设置了OnTextViewChangeListener监听器先获取当前文本选中位置的起始点然后截取从该起始点到文本末尾的内容作为新文本将编辑框内文本设置为从开头到选中起始点的内容相当于模拟了在选中位置插入新文本的操作接着调用监听器的onEditTextEnter方法并传入当前编辑框索引加1可能表示下一个编辑框的索引以及新文本内容通知外部代码回车键按下后添加新文本的情况若没有设置监听器同样打印日志提示监听器未设置。
对于其他按键抬起情况则执行父类的onKeyUp方法逻辑。
焦点变化处理函数
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)
所属类NoteEditText
功能重写了父类的方法来处理编辑框焦点变化的相关逻辑。当焦点发生变化时如果设置了OnTextViewChangeListener监听器会进一步判断
如果编辑框失去焦点focused为false并且编辑框内文本为空通过TextUtils.isEmpty方法判断则调用监听器的onTextChange方法并传入当前编辑框索引以及false通知外部代码当前编辑框文本为空且失去焦点的情况外部代码可以据此隐藏一些相关的操作选项等。
如果编辑框获得焦点或者虽然失去焦点但文本不为空则调用监听器的onTextChange方法并传入当前编辑框索引以及true通知外部代码编辑框文本状态变化情况外部代码可以据此显示相关操作选项等。
最后无论哪种情况都会调用父类的onFocusChanged方法保证原生的焦点变化相关逻辑继续执行比如可能涉及界面重绘等其他系统行为。
上下文菜单创建函数
onCreateContextMenu(ContextMenu menu)
所属类NoteEditText
功能:重写了父类的方法来创建上下文菜单(通常通过长按文本编辑框等操作触发)。在函数内部:
首先判断编辑框内的文本是否是实现了Spanned接口的类型意味着文本可能包含了一些样式、链接等特殊内容如果是则获取当前文本选中区域的起始和结束位置确定最小和最大位置。
通过getSpans方法从文本中获取位于选中区域内的URLSpan类型的对象数组URLSpan用于表示文本中的链接如果数组长度为1即只选中了一个链接则遍历预定义的链接协议与对应资源ID的映射表sSchemaActionResMap查找该链接的协议通过getURL方法获取链接字符串并判断其开头协议部分对应的资源ID如果找到则使用对应的ID如果没找到则使用默认的IDR.string.note_link_other
使用找到的资源ID通过menu.add方法在上下文菜单中添加一个菜单项并为该菜单项设置点击监听器在监听器的onMenuItemClick方法中调用对应的URLSpan对象的onClick方法通常会触发打开链接对应的页面等操作实现长按链接弹出对应操作选项并点击执行相应操作的功能最后调用父类的onCreateContextMenu方法保证原生的上下文菜单创建相关逻辑继续执行比如添加系统默认的一些菜单选项等。
接口相关方法
setIndex(int index)
所属类NoteEditText
功能用于设置当前编辑框的索引值mIndex接收一个整数参数将其赋值给mIndex变量外部代码可以通过这个方法来明确当前编辑框在一组编辑框中的顺序等信息便于后续根据索引进行相关逻辑处理比如判断是否是第一个编辑框等情况。
setOnTextViewChangeListener(OnTextViewChangeListener listener)
所属类NoteEditText
功能用于设置OnTextViewChangeListener监听器接收一个实现了该接口的对象作为参数将其赋值给mOnTextViewChangeListener变量外部代码通过实现该接口并传入对应的监听器对象就可以在文本编辑框发生文本删除、回车键按下、文本状态变化等情况时接收到通知并执行相应的业务逻辑增强了该自定义编辑框与外部代码之间的交互性。
*/
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;
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:" ;
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);
}
/**
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
public interface OnTextViewChangeListener {
/**
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
*/
void onEditTextDelete(int index, String text);
/**
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
*/
void onEditTextEnter(int index, String text);
/**
* Hide or show item option when text change
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener;
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
public void setIndex(int index) {
mIndex = index;
}
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
@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);
}
@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);
}
@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);
}
@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);
}
@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);
}
}