/* * 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,如果没找到则使用默认的ID(R.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 sSchemaActionResMap = new HashMap(); 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); } }