package jx.ym.fastedit; import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.TextPaint; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.GestureDetector; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.RelativeLayout; import android.widget.Scroller; import android.widget.Toast; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; /** * 非常快的文本编辑器 */ public class FastEdit extends RelativeLayout { public interface SelectionListener { void onSelectChanged(SelectModel selectModel); } public interface TextListener { void onInserted(int post, String text); void onDeleted(String text, int start, int end); } public int lineNumberBgColor = 0xff333333; public int lineNumberTextColor = 0xffffffff; public int textSize = 14; public int lineNumberPadding = 5; public int textColor = 0xff000000; public int editLineColor = 0x33000000; StringBuilder code; UndoStack undoStack; //光标位置 int cursorPos; //光标编辑状态的列 int cursorInputCol; Paint paint; TextPaint textPaint; //文字颜色数组 final String tabText = " "; CodeInputConnection codeInputConnection; Integer lineStarts[]; Scroller scroller; float codeScrollX, codeScrollY; InputEvent inputEvent; GestureDetector gestureDetector; float maxWidth, maxHeight; SelectModel selectModel; SelectBar selectBar; EditBar editBar; SelectionListener selectionListener; TextListener textListener; public FastEdit(Context context) { super(context); init(); } public FastEdit(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { selectBar = new SelectBar(this); editBar = new EditBar(this); setBackgroundColor(Color.WHITE); code = new StringBuilder(); undoStack = new UndoStack(); scroller = new Scroller(getContext()); inputEvent = new InputEvent(); selectModel = new SelectModel(); codeInputConnection = new CodeInputConnection(this, false); setFocusable(true); setFocusableInTouchMode(true); setLongClickable(true); paint = new Paint(); textPaint = new TextPaint(); paint.setAntiAlias(true); textPaint.setAntiAlias(true); textPaint.setTypeface(Typeface.MONOSPACE); setTextSize(textSize); gestureDetector = new GestureDetector(getContext(), new GestureListener()); } class GestureListener implements GestureDetector.OnGestureListener { float downScrollX, downScrollY; boolean fling; long lastDown; @Override public boolean onDown(MotionEvent e) { if (System.currentTimeMillis() - lastDown < 300) { onDoublePress(); return false; } lastDown = System.currentTimeMillis(); downScrollX = codeScrollX; downScrollY = codeScrollY; if (!scroller.isFinished()) { scroller.abortAnimation(); } fling = false; return true; } void onDoublePress() { lastDown = 0; select(); } /** * 手指按下 * * @param e */ @Override public void onShowPress(MotionEvent e) { } /** * 单击输入框,需要显示输入法 * 将光标位置显示在点击位置 * * @param e * @return */ @Override public boolean onSingleTapUp(MotionEvent e) { //将光标位置显示在点击位置 if (!fling) { onClickPos(e); } return false; } /** * 滑动,需要根据手势滑动界面 * * @param e1 * @param e2 * @param distanceX * @param distanceY * @return */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { setScroll(downScrollX - e2.getX() + e1.getX(), downScrollY - e2.getY() + e1.getY()); return false; } @SuppressLint("RestrictedApi") @Override public void onLongPress(MotionEvent e) { int cursorPos = getCursorPos(e.getX(), e.getY()); setCursorPos(cursorPos); showEditBar(); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { scroller.fling((int) codeScrollX, (int) codeScrollY, (int) -velocityX, (int) -velocityY, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); fling = true; return false; } } public void showEditBar() { if (selectModel.isSelected()) { return; } float cursorX = getPosX(cursorPos); float cursorY = getPosY(cursorPos); int[] location = new int[2]; getLocationInWindow(location); int left = (int) (location[0] + cursorX - getCodeScrollX()); int top = (int) (location[1] + cursorY - getCodeScrollY()); int x = left - editBar.getWidth() / 2; int y = top - editBar.getHeight(); if (y > location[1] + getHeight() - editBar.getHeight()) { y = location[1] + getHeight() - editBar.getHeight(); } if (editBar.isShowing()) { editBar.update(x, y, editBar.getWidth(), editBar.getHeight()); } else { editBar.showAtLocation(FastEdit.this, Gravity.LEFT | Gravity.TOP, x, y); } editBar.updateUndoRedoState(); } public void hideEditBar() { editBar.dismiss(); } public void select() { select(cursorPos); } public void select(int cursorPos) { int length = code.length(); //获取光标所在的字符串(文字字母或者数字可以组成字符串) char[] chars = Strings.getChars(code); if (chars == null) { return; } if (cursorPos >= length) { cursorPos = length - 1; } if (cursorPos < 0) { cursorPos = 0; } setCursorPos(cursorPos); int startPos, endPos; startPos = cursorPos; endPos = cursorPos + 1; char aChar = chars[cursorPos]; if (Strings.isRightChar(aChar)) { //往前 for (int i = startPos; i >= 0; i--) { if (Strings.isRightChar(chars[i])) { startPos = i; } else { break; } } //往后 for (int i = endPos; i < length; i++) { if (Strings.isRightChar(chars[i])) { endPos = i + 1; } else { break; } } } selectModel.setSelected(true); selectModel.select(startPos, endPos); selectModel.showSelectBar(); } protected void onClickPos(MotionEvent e) { int cursorPos = getCursorPos(e.getX(), e.getY()); if (!selectModel.isSelected() && !editBar.isShowing() && cursorPos == FastEdit.this.cursorPos) { //显示编辑菜单 showEditBar(); } else { //更新光标位置 setCursorPos(cursorPos); //更新光标列 updateCursorCol(); //取消选择 selectModel.cancel(); //重新绘制 postInvalidate(); } //显示输入法 showInputBord(); } /** * 取消选择文字 */ public void cancelSelect() { selectModel.cancel(); postInvalidate(); } Timer cursorTimer; boolean showCursor; @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); Log.d("CodeView", "onWindowFocusChanged: " + hasWindowFocus); if (cursorTimer != null) { cursorTimer.cancel(); } if (hasWindowFocus) { cursorTimer = new Timer(); cursorTimer.schedule(new TimerTask() { @Override public void run() { showCursor = !showCursor; postInvalidate(); } }, 500, 500); } } @Override public void computeScroll() { if (scroller.computeScrollOffset()) { setScroll((float) scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } } public SelectModel getSelectModel() { return selectModel; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { inputEvent.onKeyEvent(event); if (event.isCanceled()) { return true; } return super.onKeyDown(keyCode, event); } /** * 键盘↑ * * @param event */ protected void onKeyCodeDpadDown(KeyEvent event) { int line = getLine(cursorPos) + 1; if (line >= lineStarts.length) { return; } setCursorPos(getColPos(line, cursorInputCol)); if (selectModel.hasSelect()) { selectModel.cancel(); } postInvalidate(); } /** * 键盘↓ * <p> * 1.如果是shift状态,则选择文字 * 2.光标下移 * <p> * 取消选择状态:用户输入, * * @param event */ protected void onKeyCodeDpadUp(KeyEvent event) { int line = getLine(cursorPos) - 1; if (line < 0) { return; } setCursorPos(getColPos(line, cursorInputCol)); if (selectModel.hasSelect()) { selectModel.cancel(); } postInvalidate(); } public int getColPos(int line, int col) { int lineStart = getLineStart(line); int lineEnd = getLineEnd(line); int newPos = lineStart + col; if (newPos > lineEnd) { newPos = lineEnd; } return newPos; } /** * 键盘上翻页 * * @param event */ protected void onKeyCodePageUp(KeyEvent event) { codeScrollY -= getHeight(); isSide(); postInvalidate(); } /** * 键盘下翻页 * * @param event */ protected void onKeyCodePageDown(KeyEvent event) { codeScrollY += getHeight(); isSide(); postInvalidate(); } protected void onKeyCodeMoveHome(KeyEvent event) { setCursorPos(getLineStart(getLine(cursorPos))); updateCursorCol(); } protected void onKeyCodeMoveEnd(KeyEvent event) { setCursorPos(getLineEnd(getLine(cursorPos))); updateCursorCol(); } /** * 键盘← * * @param event */ protected void onKeyCodeDpadLeft(KeyEvent event) { if (selectModel.hasSelect()) { setCursorPos(selectModel.getStart()); selectModel.cancel(); } else { setCursorPos(cursorPos - 1); } postInvalidate(); updateCursorCol(); } /** * 键盘→ * * @param event */ protected void onKeyCodeDpadRight(KeyEvent event) { if (selectModel.hasSelect()) { setCursorPos(selectModel.getEnd()); selectModel.cancel(); } else { setCursorPos(cursorPos + 1); } postInvalidate(); updateCursorCol(); } /** * 键盘Enter * * @param event */ protected void onKeyCodeEnter(KeyEvent event) { insert("\n"); } /** * 键盘删除 * * @param event */ protected void onKeyCodeDel(KeyEvent event) { delete(); } /** * 键盘删除 * * @param event */ protected void onKeyCodeForwardDel(KeyEvent event) { if (selectModel.hasSelect()) { selectModel.delete(); } else { delete(cursorPos, cursorPos + 1); } } @Override public boolean onTouchEvent(MotionEvent event) { if (selectModel.onTouchEvent(event)) { return true; } return gestureDetector.onTouchEvent(event); } /** * 设置文字大小 * * @param dip */ public void setTextSize(int dip) { textPaint.setTextSize(px(dip)); } private float px(int dip) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics()); } RectF lineNumberRect; int getTextColor(int pos) { return textColor; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制背景 Drawable background = getBackground(); if (background != null) { background.draw(canvas); } long t = System.currentTimeMillis(); try { //行数量 int lineCount = getLineCount(); //绘制行号宽度 float lineNumberWidth = getLineNumberWidth(); //行高 float lineHeight = getLineHeight(); //可视文字开始行 int textVisibleStartLine = (int) (getCodeScrollY() / lineHeight); //可视文字结尾行 int textVisibleEndLine = (int) ((getHeight() + getCodeScrollY() + 0.5f) / lineHeight); if (textVisibleEndLine > lineCount - 1) { textVisibleEndLine = lineCount - 1; } //可视文字y偏移量 float textVisibleStartOffY = getCodeScrollY() - textVisibleStartLine * lineHeight; if (code != null) { //绘制文字背景 paint.setColor(Color.RED); if (selectModel.hasSelect()) { final Path selectPath = selectModel.getSelectPath(); canvas.drawPath(selectPath, paint); } //绘制文字 drawText(canvas, lineNumberWidth, lineHeight, textVisibleStartLine, textVisibleEndLine, textVisibleStartOffY); } {//绘制行号背景 if (lineNumberRect == null) { lineNumberRect = new RectF(); } // lineNumberRect.set(-getCodeScrollX(), 0, lineNumberWidth - getCodeScrollX(), getHeight()); lineNumberRect.set(0, 0, lineNumberWidth, getHeight()); paint.setColor(lineNumberBgColor); paint.setStyle(Paint.Style.FILL); canvas.drawRect(lineNumberRect, paint); } {//绘制行号 textPaint.setColor(lineNumberTextColor); for (int i = textVisibleStartLine; i <= textVisibleEndLine; i++) { float topY = (i - textVisibleStartLine) * lineHeight - textVisibleStartOffY; Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float textY = (topY - fontMetrics.ascent + fontMetrics.descent); // canvas.drawText(String.valueOf(i + 1), getLineNumberMargin() - getCodeScrollX(), textY, textPaint); canvas.drawText(String.valueOf(i + 1), getLineNumberMargin(), textY, textPaint); } } {//绘制选择 selectModel.drawSelectWater(canvas); } } finally { Log.d("CodeView", "draw use time: " + (System.currentTimeMillis() - t) + " text length:" + length()); } } /** * 绘制文字 * * @param canvas * @param lineNumberWidth * @param lineHeight * @param textVisibleStartLine * @param textVisibleEndLine * @param textVisibleStartOffY */ private void drawText(Canvas canvas, float lineNumberWidth, float lineHeight, int textVisibleStartLine, int textVisibleEndLine, float textVisibleStartOffY) { for (int i = textVisibleStartLine; i <= textVisibleEndLine; i++) { float topY = (i - textVisibleStartLine) * lineHeight - textVisibleStartOffY; int lineStart = getLineStart(i); int lineEnd = getLineEnd(i); if (lineEnd < lineStart) { lineEnd = lineStart; } float[] widths = new float[lineEnd - lineStart]; if (lineStart != lineEnd && lineStart < code.length()) { textPaint.getTextWidths(code.subSequence(lineStart, lineEnd).toString(), widths); final float width = textPaint.measureText(code.subSequence(lineStart, lineEnd).toString()) + lineNumberWidth + getLineNumberMargin(); if (width > maxWidth) { maxWidth = width; } } //绘制光标 if (cursorPos >= lineStart && cursorPos <= lineEnd && !selectModel.isSelected()) { //绘制选择行背景 drawEditLineBackground(canvas, topY, topY + lineHeight); drawCursor(canvas, lineNumberWidth, lineHeight, topY, lineStart, widths); } //绘制文字 Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float textY = (topY - fontMetrics.ascent + fontMetrics.descent); float offsetX = lineNumberWidth + getLineNumberMargin(); for (int j = lineStart; j < lineEnd; j++) { int textColor = getTextColor(j); textPaint.setColor(textColor); if (offsetX + textPaint.getTextSize() > getCodeScrollX()) { canvas.drawText(code, j, j + 1, offsetX - getCodeScrollX(), textY, textPaint); } offsetX += widths[j - lineStart]; if (offsetX > getWidth() + getCodeScrollX()) { break; } } } if (isSide()) { postInvalidate(); } } private void drawEditLineBackground(Canvas canvas, float topY, float v) { paint.setColor(editLineColor); canvas.drawRect(0, topY, getWidth(), v, paint); } private void drawCursor(Canvas canvas, float lineNumberWidth, float lineHeight, float topY, int lineStart, float[] widths) { if (showCursor && isFocused()) { //绘制光标 float off = lineNumberWidth + getLineNumberMargin(); for (int j = 0; j < (cursorPos - lineStart); j++) { off += widths[j]; } paint.setColor(Color.BLACK); canvas.drawRect(off - getCodeScrollX() - 2, topY + 5, off - getCodeScrollX() + 2, topY + lineHeight - 5, paint); } } private int getTokenCharCount(int lineEnd, int i) { int textColor = getTextColor(i); int count = 1; for (int k = i + 1; k < lineEnd; k++) { if (textColor != getTextColor(k)) { break; } count++; } return count; } /** * 返回这个,让系统认为这个界面为编辑器界面 * * @return */ @Override public boolean onCheckIsTextEditor() { return true; } /** * 返回这个,让输入法输入 * * @param outAttrs * @return */ @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { return codeInputConnection; } /** * 设置文字 * * @param text */ public void setText(String text) { delete(0, length()); if (text != null) { append(text); } } /** * 初始化行信息,统计行数量 */ private void initLines() { char[] chars = Strings.getChars(code); List<Integer> integers = new ArrayList<>(); integers.add(0); int length = code.length(); for (int i = 0; i < length; i++) { if (chars[i] == '\n') { integers.add(i + 1); } } lineStarts = integers.toArray(new Integer[0]); maxWidth = 0; maxHeight = lineStarts.length * getLineHeight(); } private void updateCursorCol() { //计算列 cursorInputCol = cursorPos - getLineStartByPos(cursorPos); } /** * 获取行数量 * * @return */ public int getLineCount() { return lineStarts == null ? 1 : lineStarts.length; } /** * 根据光标位置获取行 * * @param pos * @return */ public int getRow(int pos) { int length = code.length(); int count = 1; for (int i = 0; i < length; i++) { if (code.charAt(i) == '\n') { count++; } if (i == pos) { break; } } return count; } /** * 根据光标位置获取列 * * @param pos * @return */ public int getCol(int pos) { int length = code.length(); int count = 0; String s = code.toString(); for (int i = pos; i > 0 && i < length; i--) { if (s.charAt(i) == '\n') { break; } count++; } return count; } /** * 获取行头 * * @param pos * @return */ public int getLineStartByPos(int pos) { if (code == null) { return 0; } if (pos < 0) { return 0; } char[] chars = Strings.getChars(code); int length = code.length(); for (int i = pos - 1; i >= 0 && i < length; i--) { if (chars[i] == '\n') { return i + 1; } } return 0; } /** * 获取行尾 * * @param pos * @return */ public int getLineEndByPos(int pos) { if (code == null) { return 1; } if (pos < 0) { return 1; } int length = code.length(); char[] chars = Strings.getChars(code); for (int i = pos + 1; i < length; i++) { if (chars[i] == '\n') { return i; } } return length; } /** * 获取光标位置 * * @param x eventX * @param y eventY * @return */ public int getCursorPos(float x, float y) { if (code == null) { return 0; } //整除得到行 int line = (int) ((y + getCodeScrollY()) / getLineHeight()); int lineStart = getLineStart(line); int lineEnd = getLineEnd(line); //切割行文字 String lineText = substring(lineStart, lineEnd); if (lineText == null) { lineText = ""; } float[] widths = new float[lineText.length()]; //计算每个字宽度 textPaint.getTextWidths(lineText, widths); //获取点击点的x相对位置 float relativeX = x + getCodeScrollX() - getLineNumberMargin() - getLineNumberWidth(); float offX = 0; //相加计算最接近点击相对x的位置 for (int i = 0; i < widths.length; i++) { offX += widths[i]; if (offX - widths[i] / 2 > relativeX) { return lineStart + i; } } return lineEnd; } public int getCursorPos() { return cursorPos; } public void setCursorPos(int pos) { cursorPos = pos; if (cursorPos < 0) { cursorPos = 0; } if (cursorPos > length()) { cursorPos = length(); } onCursorChanged(); } public void undo() { undoStack.undo(); } public void redo() { undoStack.redo(); } public boolean canUndo() { return undoStack.canUndo(); } public boolean canRedo() { return undoStack.canRedo(); } boolean lockUndoStack; public void lockUndoStack() { lockUndoStack = true; } public void unlockUndoStack() { lockUndoStack = false; } public void insert(char c) { insert(cursorPos, c); } public void insert(String text) { insert(cursorPos, text); } public void insert(int pos, String text) { if (pos < 0) { return; } if (selectModel.hasSelect()) { selectModel.delete(); pos = cursorPos; } code.insert(pos, text); setCursorPos(pos + text.length()); afterTextChanged(); onInserted(pos, text); } public void clearUndoStack() { undoStack.clear(); } /** * 插入文字后触发 * * @param pos * @param text */ protected void onInserted(int pos, String text) { if (!lockUndoStack) { undoStack.add(new InsertTextStep(this, pos, text)); } if (textListener != null) { textListener.onInserted(pos, text); } } public void delete(int start, int end) { if (code == null) { return; } if (start < 0) { return; } if (end > code.length()) { end = code.length(); } String delete = code.substring(start, end); code.delete(start, end); if (delete != null) { setCursorPos(start); afterTextChanged(); onDeleted(start, end, delete); } } protected void onDeleted(int start, int end, String delete) { if (!lockUndoStack) { undoStack.add(new DeleteTextStep(this, start, delete)); } if (textListener != null) { textListener.onDeleted(delete, start, end); } } public void insert(int pos, char c) { if (selectModel.hasSelect()) { selectModel.delete(); pos = cursorPos; } code.insert(pos, String.valueOf(c)); setCursorPos(pos + 1); afterTextChanged(); onInserted(pos, String.valueOf(c)); } public void append(String text) { // code.append(text); // setCursorPos(length()); // afterTextChanged(); insert(cursorPos, text); } private void afterTextChanged() { initLines(); updateCursorCol(); scrollToVisible(); postInvalidate(); } public void onCursorChanged() { showCursor = true; scrollToVisible(); postInvalidate(); } public void selectAll() { select(0, length()); selectModel.showSelectBar(); } protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { scrollToVisible(); postInvalidate(); } } public String getText() { return code == null ? null : code.toString(); } public int length() { return code == null ? 0 : code.length(); } public String substring(int start, int end) { try { return code == null ? null : code.subSequence(start, end).toString(); } catch (Exception e) { return null; } } public void select(int start, int end) { selectModel.select(start, end); } public void deleteLine() { int lineStart = getLineStartByPos(cursorPos); int lineEnd = getLineEndByPos(cursorPos); delete(lineStart, lineEnd); } public void clear() { if (code != null) { delete(0, code.length()); } } public void gotoLine(int line) { int lineStart = getLineStart(line); setCursorPos(lineStart); } public void searchDown(String text, int startIndex) { } public void searchUp(String text, int startIndex) { } public void searchRegDown(String reg, int startIndex) { } public void searchRegUp(String reg, int startIndex) { } public void replaceSelected(String text) { } public void replaceAllText(String text, String replace) { } public void replaceAllReg(String reg, String replace) { } public void showInputBord() { InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.showSoftInput(this, 0); } public void hideInputBord() { InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); } public float getLineHeight() { Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); return (fontMetrics.bottom - fontMetrics.top) * 1.2f; } public void setLineHeight(float lineHeight) { } public float getLineNumberWidth() { int lineCount = getLineCount(); return textPaint.measureText(String.valueOf(lineCount)) + 2 * getLineNumberMargin(); } public float getLineNumberMargin() { return px(lineNumberPadding); } public float getCodeScrollX() { return codeScrollX; } public float getCodeScrollY() { return codeScrollY; } public void setScroll(float x, float y) { codeScrollX = x; codeScrollY = y; isSide(); postInvalidate(); } private boolean isSide() { boolean side = false; if (codeScrollX > maxWidth) { codeScrollX = maxWidth; side = true; } if (codeScrollY > maxHeight) { codeScrollY = maxHeight; side = true; } if (codeScrollY < 0) { codeScrollY = 0; side = true; } if (codeScrollX < 0) { codeScrollX = 0; side = true; } return side; } public int getLineStart(int line) { if (code == null || code.length() == 0 || lineStarts == null || lineStarts.length == 0 || line < 0) { return 0; } if (line < lineStarts.length) { return lineStarts[line]; } return code.length() + 1; } public int getLineEnd(int line) { int lineStart = getLineStart(line + 1); if (lineStart > 0) { return lineStart - 1; } return 0; } public void delete() { if (selectModel.hasSelect()) { selectModel.delete(); } else { delete(cursorPos - 1, cursorPos); } } public void scrollToVisible() { int line = getLine(cursorPos); float posX = getPosX(cursorPos); float posY = line * getLineHeight(); float textSize = px(this.textSize); //计算是否在可视范围之内 if (codeScrollX > posX - textSize - getLineNumberWidth()) { //在视野左边 codeScrollX = posX - textSize - getLineNumberWidth(); if (codeScrollX < 0) { codeScrollX = 0; } } if (codeScrollX < posX - getWidth() + textSize) { codeScrollX = posX - getWidth() + textSize; } if (codeScrollY < posY + getLineHeight() - getHeight()) { codeScrollY = posY + getLineHeight() - getHeight(); if (codeScrollY < 0) { codeScrollY = 0; } } if (codeScrollY > posY) { codeScrollY = posY; } } /** * 获取光标的y,真实位置需要减去scrollY * * @param cursorPos * @return */ private float getPosY(int cursorPos) { final int line = getLine(cursorPos); return line * getLineHeight(); } /** * 获取光标的x,真实位置需要减去scrollX * * @param cursorPos * @return */ private float getPosX(int cursorPos) { int lineStart = getLineStartByPos(cursorPos); int lineEnd = getLineEndByPos(cursorPos); String lineText = substring(lineStart, lineEnd); float x = getLineNumberMargin() + getLineNumberWidth(); if (lineText != null) { float[] widths = new float[lineText.length()]; textPaint.getTextWidths(lineText, widths); for (int i = 0; i < cursorPos - lineStart && i < widths.length; i++) { x += widths[i]; } } return x; } public int getLine(int cursorPos) { int line = 0; if (lineStarts != null) { for (int i = 1; i < lineStarts.length; i++) { if (cursorPos >= lineStarts[i]) { line = i; continue; } break; } } return line; } class InputEvent { void onKeyEvent(KeyEvent event) { int action = event.getAction(); int keyCode = event.getKeyCode(); switch (action) { case KeyEvent.ACTION_DOWN: { //输入模式 int unicodeChar = event.getUnicodeChar(); if (unicodeChar != 0) { input((char) unicodeChar); event.cancel(); } if (!event.isCanceled() && !event.isCtrlPressed() && !event.isAltPressed() && !event.isShiftPressed()) { switch (keyCode) { case KeyEvent.KEYCODE_TAB: //tab input(tabText); event.cancel(); break; case KeyEvent.KEYCODE_SPACE: //空格 input(' '); event.cancel(); break; case KeyEvent.KEYCODE_ENTER: //enter onKeyCodeEnter(event); event.cancel(); break; case KeyEvent.KEYCODE_DEL: //del onKeyCodeDel(event); event.cancel(); break; case KeyEvent.KEYCODE_FORWARD_DEL: //del onKeyCodeForwardDel(event); event.cancel(); break; case KeyEvent.KEYCODE_DPAD_LEFT: //left onKeyCodeDpadLeft(event); event.cancel(); break; case KeyEvent.KEYCODE_DPAD_RIGHT: //right onKeyCodeDpadRight(event); event.cancel(); break; case KeyEvent.KEYCODE_DPAD_UP: //up onKeyCodeDpadUp(event); event.cancel(); break; case KeyEvent.KEYCODE_DPAD_DOWN: //down onKeyCodeDpadDown(event); event.cancel(); break; case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_PAGE_DOWN: //page down onKeyCodePageDown(event); event.cancel(); break; case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_PAGE_UP: //page up onKeyCodePageUp(event); event.cancel(); break; case KeyEvent.KEYCODE_MOVE_HOME: onKeyCodeMoveHome(event); event.cancel(); break; case KeyEvent.KEYCODE_MOVE_END: onKeyCodeMoveEnd(event); event.cancel(); break; } } if (!event.isCanceled()) { //快捷键模式 if (onShortKey(event.isCtrlPressed(), event.isAltPressed(), event.isShiftPressed(), keyCode)) { event.cancel(); } } break; } } } /** * 快捷方式 * * @param ctrl * @param alt * @param shift * @param keyCode */ protected boolean onShortKey(boolean ctrl, boolean alt, boolean shift, int keyCode) { if (ctrl) { if (keyCode == KeyEvent.KEYCODE_A) { selectModel.select(0, length()); postInvalidate(); return true; } if (keyCode == KeyEvent.KEYCODE_C) { if (selectModel.hasSelect()) { try { copy(); } catch (Exception e) { } return true; } } if (keyCode == KeyEvent.KEYCODE_X) { if (selectModel.hasSelect()) { cut(); return true; } } if (keyCode == KeyEvent.KEYCODE_Z) { undo(); return true; } if (keyCode == KeyEvent.KEYCODE_Y) { redo(); return true; } } if (shift) { try { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { if (!selectModel.hasSelect()) { selectModel.start(cursorPos); } selectModel.toLeft(1); return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { if (!selectModel.hasSelect()) { selectModel.start(cursorPos); } selectModel.toRight(1); return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { if (!selectModel.hasSelect()) { selectModel.start(cursorPos); } selectModel.toUp(1); return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { if (!selectModel.hasSelect()) { selectModel.start(cursorPos); } selectModel.toDown(1); return true; } else if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) { if (!selectModel.hasSelect()) { selectModel.start(cursorPos); } selectModel.toHome(); return true; } else if (keyCode == KeyEvent.KEYCODE_MOVE_END) { if (!selectModel.hasSelect()) { selectModel.start(cursorPos); } selectModel.toEnd(); return true; } } finally { postInvalidate(); } } return false; } private void input(String s) { insert(s); } private void input(char c) { if (c == '\t') { input(tabText); } else { insert(c); } } } public void paste() { ClipboardManager cm = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); CharSequence text = cm.getText(); insert(text.toString()); } public String getSelectText() { int start = selectModel.getStart(); int end = selectModel.getEnd(); if (start == end || end > length()) { return null; } return code.substring(start, end); } public void copy() throws Exception { String selectText = getSelectText(); if (selectText == null) { return; } try { ClipboardManager cm = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); cm.setPrimaryClip(ClipData.newPlainText("copy", selectText)); } catch (Exception e) { Toast.makeText(getContext(), "文字太长,复制失败!", Toast.LENGTH_SHORT).show(); throw e; } } public void cut() { try { copy(); delete(); } catch (Exception e) { } } /** * 按下shift,进行文字选择 */ public class SelectModel { private int start;//可能大于end private int end; private boolean selected; private Drawable startWater; private Drawable endWater; public SelectModel() { startWater = getResources().getDrawable(R.drawable.ic_water_start); endWater = getResources().getDrawable(R.drawable.ic_water_end); } //更新选择水滴位置 private void setEndWaterBounds(Drawable water, float posX, float posY) { int wx = (int) (posX - codeScrollX); int wy = (int) (posY + getLineHeight() - codeScrollY); water.setBounds(wx, wy, (int) (wx + px(24)), (int) (wy + px(24))); } //更新选择水滴位置 private void setStartWaterBounds(Drawable water, float posX, float posY) { int wx = (int) (posX - px(24) - codeScrollX); int wy = (int) (posY + getLineHeight() - codeScrollY); water.setBounds(wx, wy, (int) (wx + px(24)), (int) (wy + px(24))); } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { if (this.selected != selected) { this.selected = selected; fireSelectionChanged(); } } public void drawSelectWater(Canvas canvas) { if (isSelected()) { startWater.draw(canvas); endWater.draw(canvas); } } boolean adjustStartWater; boolean adjustEndWater; //按下的位置与左水滴光标距离 float distanceStartWater; //按下的位置与右水滴光标距离 float distanceEndWater; public boolean onTouchEvent(MotionEvent event) { if (!isSelected()) { return false; } hideSelectBar(); float x = event.getX(); float y = event.getY(); switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: adjustEndWater = adjustStartWater = false; if (endWater.getBounds().contains((int) x, (int) y)) { //end调整 adjustEndWater = true; distanceEndWater = endWater.getBounds().left - x; } else if (startWater.getBounds().contains((int) x, (int) y)) { //start调整 adjustStartWater = true; distanceStartWater = startWater.getBounds().right - x; } else { return false; } case MotionEvent.ACTION_MOVE: //计算当前位置的光标位置,然后进行调整 if (adjustStartWater) { if (y > endWater.getBounds().bottom - getLineHeight()) { y = endWater.getBounds().bottom - getLineHeight(); } float px = x + distanceStartWater; float py = y - getLineHeight(); int cursorPos = getCursorPos(px, py); if (cursorPos < end) { start = cursorPos; } else { start = end - 1; } postInvalidate(); } else if (adjustEndWater) { if (y < startWater.getBounds().bottom - getLineHeight()) { y = startWater.getBounds().bottom - getLineHeight(); } if (y >= (getLineCount() + 0.5f) * getLineHeight() - getCodeScrollY()) { y = (getLineCount() + 0.5f) * getLineHeight() - getCodeScrollY(); } float px = x + distanceEndWater; float py = y - getLineHeight(); int cursorPos = getCursorPos(px, py); if (cursorPos > start) { end = cursorPos; } else { end = start + 1; } postInvalidate(); } else { return false; } break; case MotionEvent.ACTION_UP: if (!adjustStartWater && !adjustEndWater) { return false; } showSelectBar(); } return true; } public void showSelectBar() { if (!isSelected()) { return; } getSelectPath(); int[] location = new int[2]; getLocationInWindow(location); int left = location[0] + startWater.getBounds().right; int top = location[1] + (int) (startWater.getBounds().top - getLineHeight()); int x = left; int y = top - selectBar.getHeight(); if (y > location[1] + getHeight() - selectBar.getHeight()) { y = location[1] + getHeight() - selectBar.getHeight(); } if (selectBar.isShowing()) { selectBar.update(x, y, selectBar.getWidth(), selectBar.getHeight()); } else { selectBar.showAtLocation(FastEdit.this, Gravity.LEFT | Gravity.TOP, x, y); } } void hideSelectBar() { selectBar.dismiss(); } void fireSelectionChanged() { //显示编辑器 if (!isSelected()) { hideSelectBar(); } if (selectionListener != null) { selectionListener.onSelectChanged(this); } } public void start(int pos) { end = start = pos; fireSelectionChanged(); } public int getEnd() { return end > start ? end : start; } public int getStart() { return start > end ? end : start; } public void select(int start, int end) { this.start = start; this.end = end; setCursorPos(end); updateCursorCol(); fireSelectionChanged(); } public void cancel() { end = start; setSelected(false); updateCursorCol(); fireSelectionChanged(); } public void delete() { int sStart, sEnd; if (start < end) { sStart = start; sEnd = end; } else { sStart = end; sEnd = start; } FastEdit.this.delete(sStart, sEnd); cancel(); fireSelectionChanged(); } Path getSelectPath() { int sStart, sEnd; if (start < end) { sStart = start; sEnd = end; } else { sStart = end; sEnd = start; } Path path = new Path(); //计算 float startX = getPosX(sStart); float startY = getPosY(sStart); float endX = getPosX(sEnd); float endY = getPosY(sEnd); //设置选择水滴位置 setStartWaterBounds(startWater, startX, startY); setEndWaterBounds(endWater, endX, endY); float codeScrollX = getCodeScrollX(); if (startX - codeScrollX < 0) { startX = 0; } else if (startX - codeScrollX > getWidth()) { startX = getWidth(); } else { startX = startX - codeScrollX; } if (endX - codeScrollX < 0) { endX = 0; } else if (endX - codeScrollX > getWidth()) { endX = getWidth(); } else { endX = endX - codeScrollX; } float codeScrollY = getCodeScrollY(); if (startY - codeScrollY < -getLineHeight()) { startY = -getLineHeight(); } else if (startY - codeScrollY > getHeight()) { startY = getHeight(); } else { startY = startY - codeScrollY; } if (endY - codeScrollY < -getLineHeight()) { endY = -getLineHeight(); } else if (endY - codeScrollY > getHeight()) { endY = getHeight(); } else { endY = endY - codeScrollY; } float left = getLineNumberWidth() + getLineNumberMargin() - codeScrollX; if (left < 0) { left = 0; } path.moveTo(startX, startY); path.lineTo(startX, startY + getLineHeight()); path.lineTo(left, startY + getLineHeight()); path.lineTo(left, endY + getLineHeight()); path.lineTo(endX, endY + getLineHeight()); path.lineTo(endX, endY); path.lineTo(getWidth(), endY); path.lineTo(getWidth(), startY); path.close(); return path; } public boolean hasSelect() { if (start != end) { setSelected(true); } return isSelected(); } public void toLeft(int x) { end -= x; setCursorPos(end); updateCursorCol(); fireSelectionChanged(); } public void toRight(int x) { end += x; setCursorPos(end); updateCursorCol(); fireSelectionChanged(); } public void toDown(int i) { int line = getLine(end); line += i; if (line < 0) { return; } end = getColPos(line, cursorInputCol); setCursorPos(end); fireSelectionChanged(); } public void toUp(int i) { int line = getLine(end); line -= i; if (line < 0) { return; } end = getColPos(line, cursorInputCol); setCursorPos(end); fireSelectionChanged(); } public void toHome() { end = getLineStart(getLine(cursorPos)); setCursorPos(end); updateCursorCol(); fireSelectionChanged(); } public void toEnd() { end = getLineEnd(getLine(cursorPos)); setCursorPos(end); updateCursorCol(); fireSelectionChanged(); } } public static class InsertTextStep implements UndoStack.Step { FastEdit fastEdit; int pos; String text; public InsertTextStep(FastEdit fastEdit, int pos, String text) { this.fastEdit = fastEdit; this.pos = pos; this.text = text; } @Override public void undo() { fastEdit.lockUndoStack(); fastEdit.delete(pos, pos + text.length()); fastEdit.unlockUndoStack(); } @Override public void redo() { fastEdit.lockUndoStack(); fastEdit.insert(pos, text); fastEdit.unlockUndoStack(); } @Override public boolean merge(UndoStack.Step step) { if (step instanceof InsertTextStep) { if (((InsertTextStep) step).text.contains("\n")) { return false; } if (((InsertTextStep) step).text.contains(" ")) { return false; } if (((InsertTextStep) step).pos != pos + text.length()) { return false; } text = text + ((InsertTextStep) step).text; return true; } return false; } } public static class DeleteTextStep implements UndoStack.Step { FastEdit fastEdit; int start; String text; public DeleteTextStep(FastEdit fastEdit, int start, String text) { this.fastEdit = fastEdit; this.start = start; this.text = text; } @Override public void undo() { fastEdit.lockUndoStack(); fastEdit.insert(start, text); fastEdit.setCursorPos(start + text.length()); fastEdit.unlockUndoStack(); } @Override public void redo() { fastEdit.lockUndoStack(); fastEdit.delete(start, start + text.length()); fastEdit.setCursorPos(start); fastEdit.unlockUndoStack(); } @Override public boolean merge(UndoStack.Step step) { if (step instanceof DeleteTextStep) { if (((DeleteTextStep) step).text.contains("\n")) { return false; } if (((DeleteTextStep) step).text.contains(" ")) { return false; } if (((DeleteTextStep) step).start + ((DeleteTextStep) step).text.length() == start) { start = ((DeleteTextStep) step).start; text = ((DeleteTextStep) step).text + text; return true; } } return false; } } public static class CodeInputConnection extends BaseInputConnection { FastEdit fastEdit; public CodeInputConnection(FastEdit targetView, boolean fullEditor) { super(targetView, fullEditor); fastEdit = targetView; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { fastEdit.insert(text.toString()); return true; } /** * 屏蔽输入法按键触发键盘,适应模拟器键盘输入文字 * * @param event * @return */ @Override public boolean sendKeyEvent(KeyEvent event) { return fastEdit.onKeyDown(event.getKeyCode(), event); } @Override public boolean setSelection(int start, int end) { return true; } @Override public Editable getEditable() { return super.getEditable(); } } }