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.
xiaomi/ui/NoteEditText.java

259 lines
18 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.
*/
// 包声明表明该类所在的包名为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<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);
}
// 定义一个接口,用于与外部进行交互,当文本编辑视图发生特定的文本变更事件(如删除、添加文本、文本内容改变等情况)时,
// 会回调该接口中的相应方法供外部类如包含该文本编辑视图的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类型的链接对象数组这些链接对象代表了文本中包含的各种链接信息
// 后续会