diff --git a/NoteEditText.java b/NoteEditText.java new file mode 100644 index 0000000..91d7dca --- /dev/null +++ b/NoteEditText.java @@ -0,0 +1,452 @@ +/* + * 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. + */ + +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); + } +} + + + + + +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:"; + // 定义超文本传输协议链接的协议头(表示以网页浏览的方式打开链接)。 + private static final String SCHEME_HTTP = "http:"; + // 定义邮件链接的协议头(表示以发送邮件的方式打开链接)。 + private static final String SCHEME_EMAIL = "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); + } + + // 定义一个接口,用于与外部类(如`NoteEditActivity`)进行交互,通知外部在文本编辑框中文本发生特定变化(删除、添加、内容变更等)时进行相应处理。 + public interface OnTextViewChangeListener { + /** + * 当按下删除键(KEYCODE_DEL)且文本内容为空时,删除当前编辑文本框的相关逻辑。 + */ + void onEditTextDelete(int index, String text); + + /** + * 当按下回车键(KEYCODE_ENTER)时,在当前编辑文本框之后添加新的编辑文本框的相关逻辑。 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本内容发生变化时,根据文本是否为空来隐藏或显示相关的菜单项等操作的逻辑。 + */ + void onTextChange(int index, boolean hasText); + } + + // 用于存储实现了`OnTextViewChangeListener`接口的监听器对象,外部类可以通过设置该监听器来响应文本编辑框中的相关文本变化事件。 + private OnTextViewChangeListener mOnTextViewChangeListener; + + // 构造函数,接收上下文对象,调用父类的构造函数进行初始化,同时初始化当前编辑文本框的索引为0。 + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + // 用于设置当前编辑文本框在整个编辑列表中的索引位置。 + public void setIndex(int index) { + mIndex = index; + } + + // 用于设置文本变化监听器对象,外部类通过传入实现了`OnTextViewChangeListener`接口的实例,来接收文本编辑框中的相关文本变化通知并进行处理。 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 构造函数,接收上下文和属性集对象,调用父类对应的构造函数进行初始化,采用系统默认的编辑文本框样式(通过`android.R.attr.editTextStyle`指定)。 + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 构造函数,接收上下文、属性集和默认样式资源ID,调用父类对应的构造函数进行初始化,此处`TODO Auto-generated constructor stub`表示可能需要后续补充一些特定的初始化逻辑(目前为空)。 + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub + } + + // 重写父类的`onTouchEvent`方法,用于处理触摸事件,主要实现了点击文本区域时根据触摸位置设置文本光标位置的功能,以便用户能方便地在指定位置进行文本编辑操作。 + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // 获取触摸点相对于文本编辑框左边的距离(去除文本框的内边距等影响),并考虑滚动偏移量,得到在文本内容区域内的横坐标位置。 + int x = (int) event.getX(); + x -= getTotalPaddingLeft(); + x += getScrollX(); + + // 获取触摸点相对于文本编辑框上边的距离(去除文本框的内边距等影响),并考虑滚动偏移量,得到在文本内容区域内的纵坐标位置。 + int y = (int) event.getY(); + y -= getTotalPaddingTop(); + 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); + } + + // 重写父类的`onKeyDown`方法,用于处理按键按下事件,在这里主要是记录按下删除键(KEYCODE_DEL)时文本选择的起始位置,以及在按下回车键(KEYCODE_ENTER)时进行一些特定的逻辑处理(目前是返回false,可能后续会添加更多逻辑)。 + @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); + } + + // 重写父类的`onKeyUp`方法,用于处理按键抬起事件,在这里根据抬起的按键不同执行不同的逻辑, + // 比如按下删除键抬起时,如果文本框索引不为0且文本选择起始位置为0(可能表示要删除当前文本框等情况),则通知监听器执行删除逻辑; + // 按下回车键抬起时,获取当前光标位置后的文本内容,将其作为新添加文本框的初始内容,并通知监听器执行添加文本框的逻辑。 + @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); + } + + // 重写父类的`onFocusChanged`方法,用于处理文本编辑框焦点变化事件,在这里根据焦点是否失去以及文本内容是否为空,通知监听器执行相应的文本变化处理逻辑(比如隐藏或显示相关菜单项等)。 + @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); + } + + // 重写父类的`onCreateContextMenu`方法,用于创建上下文菜单(长按文本时弹出的菜单),在这里主要是处理文本中包含链接(通过`URLSpan`标识)的情况, + // 如果选中的文本区域内只有一个链接,根据链接的协议头查找对应的菜单描述资源ID,并添加一个菜单项用于点击链接执行相应操作(如打开网页、拨打电话、发送邮件等)。 + @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) { + // 点击菜单项时,执行链接对应的点击操作,比如打开相应的网页、拨打电话或者发送邮件等,调用URLSpan的onClick方法来触发具体操作。 + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } +} \ No newline at end of file