/* * 版权声明部分,表明代码的版权归属为The MiCode Open Source Community(米柚开源社区),遵循Apache License 2.0协议, * 该协议规定了代码使用、分发等方面的权限和限制条件等内容,这里声明了代码在满足协议要求的情况下可被使用。 * 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; // 导入Android系统的上下文相关类,用于获取应用的各种资源、与系统服务交互等,例如获取字符串资源、启动其他组件等操作都依赖于此。 import android.content.Context; // 导入用于表示矩形区域的类,在处理视图的边界、触摸区域等方面可能会用到,比如确定某个触摸点是否在特定的矩形范围内等情况。 import android.graphics.Rect; // 导入文本布局相关的类,可通过它获取文本在视图中的行信息、字符偏移量等内容,有助于进行基于文本布局的操作,比如根据触摸位置确定对应的文本位置。 import android.text.Layout; // 导入文本选择相关的类,用于设置文本的选中区域,比如设置光标位置、选中一段文本等操作都可以通过它提供的方法来实现。 import android.text.Selection; // 导入处理包含样式等复杂文本的接口,用于判断文本是否包含特定样式(如这里后续会涉及的URL样式等)以及获取相关样式元素等操作。 import android.text.Spanned; // 导入文本工具类,提供了一些便捷的文本处理方法,例如判断文本是否为空字符串等常用操作,方便代码中对文本状态的判断。 import android.text.TextUtils; // 导入用于处理文本中URL样式的类,通过它可以识别文本里的链接样式,并进行相应的操作,比如点击链接跳转等功能实现会用到它。 import android.text.style.URLSpan; // 导入Android系统的工具类,主要用于记录日志信息,方便在开发调试阶段查看程序运行情况,定位问题所在。 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; // 导入Android系统默认的文本编辑框类,本自定义的NoteEditText类继承自它,以在其基础上扩展更多符合特定需求的功能。 import android.widget.EditText; // 导入项目自定义的资源相关类,通过它可以获取在项目中定义的各种资源,比如字符串资源(这里后续用于获取不同链接类型对应的提示文本资源ID等)。 import net.micode.notes.R; // 导入Java中的HashMap类和Map接口,用于创建键值对形式的集合,这里用来存储不同链接协议与对应的资源ID的映射关系,方便查找使用。 import java.util.HashMap; import java.util.Map; // 自定义的文本编辑框类,继承自Android系统的EditText类,目的是在原生文本编辑框功能基础上进行定制化扩展,以满足特定应用场景(可能是笔记应用中编辑文本的特殊需求)的功能要求。 public class NoteEditText extends EditText { // 定义一个静态的、用于日志记录的标签字符串常量,其值为"NoteEditText",在使用Log输出日志时,通过这个标签可以方便地识别出是该类中产生的日志信息,便于调试和问题排查。 private static final String TAG = "NoteEditText"; // 用于记录当前这个NoteEditText实例在一组编辑文本框中的索引位置,例如在多个编辑文本框组成的列表里,通过该索引可以区分不同的文本框,方便进行相关操作和管理。 private int mIndex; // 用于记录在删除操作之前,文本选择区域的起始位置,后续在处理删除相关逻辑时,可以依据这个位置来判断是否满足删除当前文本框等条件,辅助进行更精准的操作判断。 private int mSelectionStartBeforeDelete; // 定义表示电话链接协议的常量字符串,其值为"tel:",用于在文本中识别是否存在电话链接相关内容,以便后续进行相应的处理(如根据链接类型展示不同提示等)。 private static final String SCHEME_TEL = "tel:" ; // 定义表示HTTP网页链接协议的常量字符串,其值为"http:",用于判断文本里是否包含网页链接,进而采取对应的操作逻辑,比如点击链接跳转到网页等。 private static final String SCHEME_HTTP = "http:" ; // 定义表示邮件链接协议的常量字符串,其值为"mailto:",用于识别文本中的邮件链接情况,方便实现点击邮件链接启动邮件客户端等相关功能。 private static final String SCHEME_EMAIL = "mailto:" ; // 创建一个静态的HashMap类型的集合,用于存储不同链接协议(如上述的tel、http、mailto等)与对应的资源ID(这些资源ID通常关联着在界面上显示的对应链接类型的提示字符串等资源)之间的映射关系,方便后续根据链接协议快速查找并获取相应的资源显示信息。 private static final Map sSchemaActionResMap = new HashMap(); // 静态代码块,用于在类加载时初始化上面定义的sSchemaActionResMap集合,将不同链接协议与对应的资源ID进行关联赋值,使得后续可以直接通过协议字符串查找到对应的资源ID,例如tel协议对应R.string.note_link_tel这个资源ID,以此类推。 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); } // 定义一个内部接口,名为OnTextViewChangeListener,用于作为文本编辑框文本变化的监听器,规定了在特定文本编辑操作发生时(如删除、新增文本、文本内容变化等情况)需要外部实现的回调方法, // 这样外部类(比如包含该编辑文本框的Activity等)可以通过实现这个接口,并将实现对象设置给该编辑文本框,来监听并处理相应的文本变化逻辑,实现业务上的解耦和功能扩展。 /** * 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 */ // 定义当按下删除键(对应KeyEvent类中的KEYCODE_DEL常量)并且当前文本编辑框中的文本内容为空时需要调用的方法, // 外部实现该接口的类需要根据具体业务场景来编写这个方法的具体逻辑,比如在笔记应用中可能是从编辑文本框列表里移除当前这个空文本框等操作。 void onEditTextDelete(int index, String text); /** * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} * happen */ // 定义当按下回车键(对应KeyEvent类中的KEYCODE_ENTER常量)时需要调用的方法,外部实现该接口的类要根据业务需求来实现具体的添加文本框逻辑, // 例如在笔记应用中可能是创建一个新的编辑文本框并添加到合适的位置(通常是当前文本框之后),方便用户继续输入内容等操作。 void onEditTextEnter(int index, String text); /** * Hide or show item option when text change */ // 定义当文本内容发生变化时需要调用的方法,外部实现该接口的类要依据文本是否有内容等情况来决定相关操作选项(比如上下文菜单里的某些选项、界面上某些与文本相关的按钮等)的显示或隐藏状态,以提供符合当前文本状态的交互界面。 void onTextChange(int index, boolean hasText); } // 定义一个成员变量,用于存储实现了OnTextViewChangeListener接口的监听器对象,通过调用setOnTextViewChangeListener方法可以将外部实现的监听器对象赋值给它, // 从而实现文本编辑框文本变化事件与外部处理逻辑的关联,让外部类能够监听并处理文本编辑框的各种文本变化相关情况。 private OnTextViewChangeListener mOnTextViewChangeListener; // 构造函数,接收一个Context类型的参数,用于创建NoteEditText实例时传入上下文信息,调用父类(EditText)的构造函数,传入上下文和null作为属性集(通常在简单创建实例且不需要从XML布局中加载属性时使用这种方式), // 同时初始化当前编辑文本框的索引值mIndex为0,表示默认的初始索引位置。 public NoteEditText(Context context) { super(context, null); mIndex = 0; } // 设置当前编辑文本框索引值的方法,外部可以通过调用该方法传入新的索引值来更新mIndex变量,方便在多个编辑文本框组成的场景中,准确地对每个文本框进行定位和管理,例如在列表中调整文本框顺序等操作时会用到。 public void setIndex(int index) { mIndex = index; } // 设置文本变化监听器的方法,外部类(如包含该编辑文本框的Activity等)可以通过调用该方法,传入实现了OnTextViewChangeListener接口的对象,将该对象赋值给mOnTextViewChangeListener变量, // 这样当文本编辑框发生文本删除、新增、内容变化等相关事件时,就能触发对应的回调方法,执行外部类中定义的相应业务逻辑了。 public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } // 构造函数,接收上下文对象(Context)和属性集(AttributeSet)作为参数,调用父类(EditText)的构造函数,传入上下文、属性集以及默认的编辑文本框样式(通过android.R.attr.editTextStyle指定), // 这种构造方式常用于在XML布局文件中定义该自定义编辑文本框时,加载XML中配置的属性并应用默认样式来初始化文本框实例,使其具有合适的外观和初始属性设置。 public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } // 构造函数,接收上下文对象(Context)、属性集(AttributeSet)和默认样式定义(defStyle)作为参数,调用父类(EditText)的构造函数,传入相应参数进行初始化, // 不过当前构造函数中的具体实现部分是自动生成的占位代码(由TODO注释标识,意味着后续可能需要根据具体业务需求进一步完善此处的初始化逻辑,比如根据传入的defStyle进行更多样式相关的设置等)。 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) { // 根据触摸事件的动作类型(通过event.getAction()方法获取)进行不同的处理逻辑分支,这里主要关注MotionEvent.ACTION_DOWN(即触摸按下动作)的情况, // 对于其他触摸动作类型(如触摸滑动、触摸抬起等),则默认按照父类(EditText)原有的处理逻辑执行(通过调用super.onTouchEvent(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(); // 根据经过调整后的触摸点纵坐标(y),通过布局对象的getLineForVertical方法,获取触摸点所在的文本行索引,即确定触摸位置处于文本的第几行,方便后续进一步定位该行内的具体字符位置。 int line = layout.getLineForVertical(y); // 根据触摸点所在的文本行索引(line)以及经过调整后的横坐标(x),通过布局对象的getOffsetForHorizontal方法,获取触摸点对应的文本字符在整个文本字符串中的偏移量(也就是字符位置索引), // 这个偏移量能够准确地指出触摸位置对应的是文本中的哪个字符,为后续设置文本选中区域提供准确的位置信息。 int off = layout.getOffsetForHorizontal(line, x); // 通过Selection类的setSelection方法,依据获取到的字符偏移量(off)来设置文本的选中区域,即将文本光标定位到触摸点对应的字符位置,或者如果有长按等操作逻辑,也可以实现选中从触摸点开始的一段文本等效果, // 方便用户后续进行复制、删除、粘贴等文本编辑操作,提升文本编辑的交互性和便捷性。 Selection.setSelection(getText(), off); break; } // 返回调用父类(EditText)的onTouchEvent方法的结果,这样既保证了在处理完特定触摸动作(如这里的ACTION_DOWN)的自定义逻辑后,还能让父类继续处理触摸事件相关的其他默认逻辑, // 例如触摸事件的传递、与系统其他组件的交互等功能,确保整个触摸事件处理流程的完整性和正确性,避免影响其他相关的触摸操作功能。 return super.onTouchEvent(event); } @Override // 重写onKeyDown方法,用于处理按键按下的事件 public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: // 当按下回车键时,如果存在文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 if (mOnTextViewChangeListener!= null) { // 直接返回false,这里可能是根据具体业务逻辑决定不做额外处理,直接向上传递该返回值 return false; } break; case KeyEvent.KEYCODE_DEL: // 当按下删除键(DEL)时,记录当前删除操作前的文本选择起始位置,以便后续可能的业务逻辑使用 mSelectionStartBeforeDelete = getSelectionStart(); break; default: break; } // 如果上述case中没有处理或者需要默认行为,调用父类的onKeyDown方法继续处理 return super.onKeyDown(keyCode, event); } @Override // 重写onKeyUp方法,用于处理按键抬起的事件 public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DEL: // 当删除键(DEL)抬起时,如果文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 if (mOnTextViewChangeListener!= null) { // 判断如果当前选择起始位置为0 并且 mIndex不等于0(这里mIndex具体含义需看上下文,可能是某种索引标识) if (0 == mSelectionStartBeforeDelete && mIndex!= 0) { // 调用监听器的onEditTextDelete方法,传递当前索引(mIndex)以及文本内容(通过getText().toString()获取),并返回true表示已经处理该事件 mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); return true; } } else { // 如果监听器为空,打印日志提示监听器未设置 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; case KeyEvent.KEYCODE_ENTER: // 当回车键抬起时,如果文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 if (mOnTextViewChangeListener!= null) { // 获取当前文本选择的起始位置 int selectionStart = getSelectionStart(); // 获取从选择起始位置到文本末尾的子字符串,即回车键之后的文本内容 String text = getText().subSequence(selectionStart, getText().length()).toString(); // 将文本内容设置为从开头到选择起始位置的子字符串,相当于删除了回车键之后的文本内容 setText(getText().subSequence(0, selectionStart)); // 调用监听器的onEditTextEnter方法,传递下一个索引(mIndex + 1)以及回车键之后的文本内容(text) mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); } else { // 如果监听器为空,打印日志提示监听器未设置 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; default: break; } // 如果上述case中没有处理或者需要默认行为,调用父类的onKeyUp方法继续处理 return super.onKeyUp(keyCode, event); } @Override // 重写onFocusChanged方法,用于处理焦点改变的事件 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { // 如果文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 if (mOnTextViewChangeListener!= null) { // 如果当前失去焦点(!focused)并且文本内容为空(TextUtils.isEmpty(getText())) if (!focused && TextUtils.isEmpty(getText())) { // 调用监听器的onTextChange方法,传递当前索引(mIndex)以及表示文本为空的布尔值(false) mOnTextViewChangeListener.onTextChange(mIndex, false); } else { // 如果不是上述情况(即获得焦点或者文本不为空),调用监听器的onTextChange方法,传递当前索引(mIndex)以及表示文本不为空的布尔值(true) mOnTextViewChangeListener.onTextChange(mIndex, true); } } // 调用父类的onFocusChanged方法继续处理焦点改变相关的其他默认逻辑 super.onFocusChanged(focused, direction, previouslyFocusedRect); } @Override // 重写onCreateContextMenu方法,用于创建上下文菜单(通常是长按文本等操作弹出的菜单) protected void onCreateContextMenu(ContextMenu menu) { // 如果文本内容是Spanned类型(Spanned通常用于包含富文本信息,比如包含链接等格式的文本) if (getText() instanceof Spanned) { // 获取当前文本选择的起始位置 int selStart = getSelectionStart(); // 获取当前文本选择的结束位置 int selEnd = getSelectionEnd(); // 获取选择范围的最小值(起始和结束位置中较小的那个) int min = Math.min(selStart, selEnd); // 获取选择范围的最大值(起始和结束位置中较大的那个) int max = Math.max(selStart, selEnd); // 获取在选择范围内的所有URLSpan类型的对象数组,URLSpan通常用于表示文本中的链接信息 final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); if (urls.length == 1) { int defaultResId = 0; // 遍历sSchemaActionResMap的键集合(这里sSchemaActionResMap具体含义需看上下文,可能是某种资源映射关系) for (String schema : sSchemaActionResMap.keySet()) { // 如果URLSpan中的URL包含当前遍历到的模式(schema) if (urls[0].getURL().indexOf(schema) >= 0) { // 获取对应的资源ID defaultResId = sSchemaActionResMap.get(schema); break; } } if (defaultResId == 0) { // 如果没有找到匹配的资源ID,设置默认的资源ID(这里是R.string.note_link_other,具体字符串资源需看项目中的定义) defaultResId = R.string.note_link_other; } // 向上下文菜单中添加一个菜单项,参数依次为:组ID(0)、菜单项ID(0)、排序顺序(0)、显示的字符串资源ID(defaultResId) menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { // 当点击该菜单项时,调用URLSpan的onClick方法,传入当前NoteEditText实例(this),通常用于触发链接跳转等操作 urls[0].onClick(NoteEditText.this); return true; } }); } } // 调用父类的onCreateContextMenu方法继续处理上下文菜单创建的其他默认逻辑 super.onCreateContextMenu(menu); }