/* * 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. */ // 包声明,表明该类所在的包名为net.micode.notes.ui 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; // NoteEditText类继承自EditText,它是一个自定义的文本编辑视图类,在基础的文本编辑功能之上,添加了一些与特定业务逻辑相关的功能, // 比如处理不同类型链接的点击、根据按键操作触发相应的文本变更回调等功能,用于在笔记编辑场景下更灵活地处理文本输入与交互操作。 public class NoteEditText extends EditText { // 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息,便于调试和查看运行情况。 private static final String TAG = "NoteEditText"; // 用于记录当前该文本编辑视图在整体布局中的索引位置,例如在列表模式下多个文本编辑视图组成列表时,标识其顺序位置,方便进行相关操作和逻辑处理。 private int mIndex; // 用于记录在按下删除键(KEYCODE_DEL)之前文本的选择起始位置,以便后续判断是否执行特定的删除逻辑,比如在特定条件下触发删除整个文本编辑视图等操作。 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:"; // 用于存储不同链接协议(如tel、http、mailto等)与对应的资源ID(通常用于显示给用户的操作提示字符串资源)的映射关系, // 方便根据链接类型获取相应的提示文本资源,例如在弹出的菜单中显示合适的操作选项名称。 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); } // 定义一个接口,用于与外部进行交互,当文本编辑视图发生特定的文本变更事件(如删除、添加文本、文本内容改变等情况)时, // 会回调该接口中的相应方法,供外部类(如包含该文本编辑视图的Activity)进行相关逻辑处理,实现对文本编辑操作的监听和响应。 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); } // 用于持有实现了OnTextViewChangeListener接口的实例,外部类通过设置该实例来监听文本编辑视图的相关文本变更事件,以便进行对应的业务逻辑处理。 private OnTextViewChangeListener mOnTextViewChangeListener; // 构造函数,接收上下文(Context)参数,调用父类(EditText)的另一个构造函数进行初始化,同时将索引(mIndex)初始化为0, // 该构造函数通常在代码中通过 `new` 关键字创建实例时使用,用于简单创建一个文本编辑视图实例并设置初始索引值。 public NoteEditText(Context context) { super(context, null); mIndex = 0; } // 用于设置当前文本编辑视图在整体布局中的索引位置的方法,外部类可以通过调用该方法来更新索引值,确保在进行相关操作时能准确识别每个文本编辑视图的顺序和位置。 public void setIndex(int index) { mIndex = index; } // 用于设置实现了OnTextViewChangeListener接口的监听器实例的方法,外部类通过传入实现了该接口的对象, // 使得文本编辑视图能够在特定文本变更事件发生时回调对应的接口方法,实现对文本编辑操作的监听和响应。 public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } // 构造函数,接收上下文(Context)和属性集(AttributeSet)参数,调用父类(EditText)的对应构造函数进行初始化, // 并采用系统默认的编辑文本样式(android.R.attr.editTextStyle)来设置文本编辑视图的外观和基本属性, // 该构造函数通常在布局文件中通过 XML 配置创建该视图实例时被调用,以解析并应用布局中定义的相关属性设置。 public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } // 构造函数,接收上下文(Context)、属性集(AttributeSet)和默认样式(defStyle)参数,调用父类(EditText)的对应构造函数进行初始化, // 该构造函数提供了更灵活的样式定制功能,可根据传入的默认样式参数来设置文本编辑视图的外观和属性,不过这里暂未做额外的自定义初始化逻辑(TODO 部分)。 public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub } // 重写父类(EditText)的触摸事件处理方法(onTouchEvent),用于处理用户在文本编辑视图上的触摸操作, // 这里主要实现了根据触摸位置设置文本选择的功能,例如用户点击文本中的某个位置时,将光标定位到对应的位置。 @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 获取触摸点的原始 X 坐标(相对于文本编辑视图左上角)。 int x = (int) event.getX(); // 获取触摸点的原始 Y 坐标(相对于文本编辑视图左上角)。 int y = (int) event.getY(); // 调整 X 坐标,减去文本编辑视图的左内边距,使其坐标基于文本内容区域的左上角。 x -= getTotalPaddingLeft(); // 调整 Y 坐标,减去文本编辑视图的上内边距,使其坐标基于文本内容区域的左上角。 y -= getTotalPaddingTop(); // 进一步调整 X 坐标,加上文本编辑视图的水平滚动偏移量,以处理滚动情况下的正确位置计算。 x += getScrollX(); // 进一步调整 Y 坐标,加上文本编辑视图的垂直滚动偏移量,以处理滚动情况下的正确位置计算。 y += getScrollY(); // 获取文本编辑视图的文本布局对象,用于后续根据坐标计算文本位置相关操作。 Layout layout = getLayout(); // 根据触摸点的垂直坐标(Y坐标)获取对应的文本行号,确定触摸点所在的文本行。 int line = layout.getLineForVertical(y); // 根据触摸点所在的文本行以及水平坐标(X坐标)获取对应的文本偏移量,即确定触摸点在该行文本中的具体位置偏移量。 int off = layout.getOffsetForHorizontal(line, x); // 根据计算得到的文本偏移量,设置文本的选择位置,即将光标定位到触摸点对应的文本位置处,方便用户进行后续的文本编辑操作。 Selection.setSelection(getText(), off); break; } // 调用父类(EditText)的onTouchEvent方法,确保其他默认的触摸事件处理逻辑也能正常执行,例如处理长按弹出复制粘贴等菜单操作等。 return super.onTouchEvent(event); } // 重写父类(EditText)的按键按下事件处理方法(onKeyDown),用于捕获特定按键按下的操作, // 这里主要针对回车键(KEYCODE_ENTER)和删除键(KEYCODE_DEL)进行了相关逻辑处理,例如记录一些必要的状态信息等。 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: // 如果设置了OnTextViewChangeListener监听器,返回false,可能是为了让后续的默认回车键处理逻辑(如换行等)先不执行, // 而是等待在按键抬起(onKeyUp)时由自定义的逻辑来处理回车键相关操作,比如添加新的文本编辑视图等。 if (mOnTextViewChangeListener!= null) { return false; } break; case KeyEvent.KEYCODE_DEL: // 当按下删除键时,记录当前文本的选择起始位置,用于后续在按键抬起时判断是否执行特定的删除操作,例如删除整个文本编辑视图等情况。 mSelectionStartBeforeDelete = getSelectionStart(); break; default: break; } // 调用父类(EditText)的onKeyDown方法,确保其他默认的按键按下事件处理逻辑也能正常执行,例如处理其他按键的功能(如方向键移动光标等)。 return super.onKeyDown(keyCode, event); } // 重写父类(EditText)的按键抬起事件处理方法(onKeyUp),用于在特定按键抬起时执行相应的业务逻辑, // 这里针对删除键(KEYCODE_DEL)和回车键(KEYCODE_ENTER)进行了详细的操作处理,根据不同情况触发相应的文本变更回调方法等。 @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DEL: if (mOnTextViewChangeListener!= null) { // 判断如果在按下删除键之前文本的选择起始位置为0(意味着可能要删除整个文本内容)且当前文本编辑视图的索引不为0(不是第一个视图), // 则调用OnTextViewChangeListener接口的onEditTextDelete方法,通知外部类进行相应的删除操作,例如从布局中移除该文本编辑视图等, // 并返回true表示该按键事件已被处理,阻止父类的默认删除键抬起处理逻辑(通常是删除单个字符等操作)执行。 if (0 == mSelectionStartBeforeDelete && mIndex!= 0) { mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); return true; } } else { // 如果没有设置OnTextViewChangeListener监听器,则在日志中记录提示信息,便于调试发现问题,因为此时无法执行相应的自定义删除逻辑。 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; case KeyEvent.KEYCODE_ENTER: if (mOnTextViewChangeListener!= null) { // 获取当前文本的选择起始位置,用于后续截取要添加到新文本编辑视图中的文本内容。 int selectionStart = getSelectionStart(); // 截取从选择起始位置到文本末尾的内容作为要添加到新文本编辑视图中的文本,通过调用subSequence方法获取子串并转换为字符串类型。 String text = getText().subSequence(selectionStart, length()).toString(); // 将当前文本编辑视图中的文本内容更新为从开头到选择起始位置的部分,相当于把要添加到新视图的文本分离出来, // 后续会通过onEditTextEnter方法将该文本添加到新创建的文本编辑视图中。 setText(getText().subSequence(0, selectionStart)); // 调用OnTextViewChangeListener接口的onEditTextEnter方法,通知外部类添加新的文本编辑视图及相应文本内容, // 传入当前索引加1作为新视图的索引,表示添加在当前视图之后,以及截取的文本内容作为新视图的初始文本。 mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); } else { // 如果没有设置OnTextViewChangeListener监听器,则在日志中记录提示信息,便于调试发现问题,因为此时无法执行相应的自定义回车键相关逻辑。 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; default: break; } // 调用父类(EditText)的onKeyUp方法,确保其他默认的按键抬起事件处理逻辑也能正常执行,例如处理按键抬起后的一些状态重置等操作。 return super.onKeyUp(keyCode, event); } // 重写父类(EditText)的焦点改变事件处理方法(onFocusChanged),用于在文本编辑视图的焦点发生改变(获得焦点或失去焦点)时执行相应的逻辑, // 这里根据焦点状态以及文本内容是否为空,调用OnTextViewChangeListener接口的onTextChange方法通知外部类进行相关操作,例如显示或隐藏某些菜单项等。 @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (mOnTextViewChangeListener!= null) { // 如果失去焦点且文本内容为空,调用OnTextViewChangeListener接口的onTextChange方法,通知外部类进行相应操作, // 例如隐藏与文本编辑相关的功能选项等,传入当前索引以及表示文本为空的标志(false)。 if (!focused && TextUtils.isEmpty(getText())) { mOnTextViewChangeListener.onTextChange(mIndex, false); } else { // 如果获得焦点或者文本内容不为空,调用OnTextViewChangeListener接口的onTextChange方法,通知外部类进行相应操作, // 例如显示与文本编辑相关的功能选项等,传入当前索引以及表示文本不为空的标志(true)。 mOnTextViewChangeListener.onTextChange(mIndex, true); } } // 调用父类(EditText)的onFocusChanged方法,确保其他默认的焦点改变事件处理逻辑也能正常执行,例如处理焦点改变后的一些界面更新等操作。 super.onFocusChanged(focused, direction, previouslyFocusedRect); } // 重写父类(EditText)的创建上下文菜单方法(onCreateContextMenu),用于在长按文本编辑视图弹出上下文菜单时,添加自定义的菜单项及相应的点击处理逻辑, // 这里主要针对文本中包含的链接(URLSpan类型)进行处理,根据链接类型添加对应的操作菜单项,例如点击电话号码链接可拨打电话等操作。 @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); // 从文本的选中范围(由min和max确定)内获取所有的URLSpan类型的链接对象数组,这些链接对象代表了文本中包含的各种链接信息, // 后续会