不多说,直接上代码:
需要说明的是,涉及到效率问题主要是在setText()和setPadding(),该方法用于调整行号的宽度,需要先知道最大行号数字的长度。所以需要先setText()把内容显示在EditText中,再通过getLineCount()获取最大行号,然后再setPadding(),这样的效率比较低,对于大量的数据可能会出现ANR。
解决方案就是把最大行号和内容一起保存起来,下次读取内容的时候先通过最大行号setPadding(),再setText()。
NoteEditText.java
public class NoteEditText extends EditText {
private Context context;
private boolean initialized;
// 画笔 用来画下划线
private Paint paint;
private Paint mNumPaint;
private Paint mRectPaint;
private int mNumberLength;
private float mTextSize; // sp
private ScrollView mScrollView;
public static final String KEY_TEXT_SIZE = "textSize";
private String KEY_NUMBER_LENGTH = "_numberLength";
public static final String KEY_SHOW_LINE = "showLine";
public static final String KEY_SHOW_LINE_NUMBER = "showLineNumber";
public static final float DEFAULT_VALUE_TEXT_SIZE = 20;
public static final boolean DEFAULT_VALUE_SHOW_LINE = true;
public static final boolean DEFAULT_VALUE_SHOW_LINE_NUMBER = false;
private boolean showLine;
private boolean showLineNumber;
private int mPaddingLeft;
private static final int LINE_OFFSET = 50;
float displayPaddingLeft;
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
setTextCursor();
}
// 自定义光标
private void setTextCursor() {
try {
Method createEditorIfNeeded = ReflectionUtils.getMethod(
"android.widget.TextView", "createEditorIfNeeded", null);
if (createEditorIfNeeded != null) {
createEditorIfNeeded.setAccessible(true);
createEditorIfNeeded.invoke(this, new Object[0]);
Field editor = ReflectionUtils.getField(
"android.widget.TextView", "mEditor");
if (editor != null) {
editor.setAccessible(true);
Field cursorDrawable = ReflectionUtils.getField(
"android.widget.Editor", "mCursorDrawable");
if (cursorDrawable != null) {
cursorDrawable.setAccessible(true);
Array.set(cursorDrawable.get(editor.get(this)), 0,
new LineSpaceCursorDrawable(context, this));
Array.set(cursorDrawable.get(editor.get(this)), 1,
new LineSpaceCursorDrawable(context, this));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void init() {
showLine = Cache.read_Boolean(KEY_SHOW_LINE, DEFAULT_VALUE_SHOW_LINE);
if (showLine) {
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(DisplayUtils.dip2px(context, 0.5f));
paint.setColor(context.getResources().getColor(
R.color.note_edittext_line_color));
// 开启抗锯齿 较耗内存
paint.setAntiAlias(true);
}
showLineNumber = Cache.read_Boolean(KEY_SHOW_LINE_NUMBER,
DEFAULT_VALUE_SHOW_LINE_NUMBER);
if (showLineNumber) {
mNumPaint = new Paint();
mNumPaint.setStyle(Paint.Style.FILL);
mNumPaint.setColor(context.getResources().getColor(
R.color.note_edittext_line_number_color));
// 开启抗锯齿 较耗内存
mNumPaint.setAntiAlias(true);
mPaddingLeft = getPaddingLeft();
mRectPaint = new Paint();
mRectPaint.setStyle(Paint.Style.FILL);
mRectPaint.setColor(context.getResources().getColor(
R.color.note_edittext_line_number_rect_color));// Color.rgb(153,
// 148,
// 252));
}
displayPaddingLeft = DisplayUtils.dip2px(context, 5);
setTextSize(Cache.read_float(KEY_TEXT_SIZE, DEFAULT_VALUE_TEXT_SIZE));
}
// 添加弹性效果
public void setScrollView(ScrollView scrollView) {
mScrollView = scrollView;
}
@Override
protected void onDraw(Canvas canvas) {
try {
if (!showLine && !showLineNumber)
return;
if (!initialized)
return;
// 得到总行数
int lineCount = getLineCount();
// 得到每行的高度
int lineHeight = getLineHeight();
int lineStart = 0;
if (mScrollView != null) {
lineStart = (mScrollView.getScrollY() - getPaddingTop())
/ lineHeight - LINE_OFFSET;
lineStart = lineStart >= 0 ? lineStart : 0;
}
int lineEnd = lineStart
+ ScreenInfo.getInstance().getHeightPixels() / lineHeight
+ LINE_OFFSET * 2;
if (showLineNumber) {
int numberLength = getNumberLength(lineCount);
numberLength = numberLength > 2 ? numberLength : 2;
if (numberLength != mNumberLength) {
Logger.d("numberLength != mNumberLength lineCount:"
+ lineCount + " getVisibility:"
+ this.getVisibility());
mNumberLength = numberLength;
Cache.write_int(KEY_NUMBER_LENGTH, mNumberLength);
adjustPadding();
}
// 绘制行号背景
canvas.drawRect(new Rect(0, mScrollView.getScrollY()
- (int) DisplayUtils.dip2px(context, 600), mPaddingLeft
- (int) displayPaddingLeft, mScrollView.getScrollY()
+ ScreenInfo.getInstance().getHeightPixels()
+ (int) DisplayUtils.dip2px(context, 600)), mRectPaint);
float textWidth = getNumberTextWidth(mNumPaint);
for (int i = lineStart; i < lineEnd; i++) {
int x = (int) (mPaddingLeft - displayPaddingLeft
- DisplayUtils.dip2px(context, 15) / 2 - getNumberLength(i + 1)
* textWidth);
int y = ((i + 1) * getLineHeight()) - (getLineHeight() / 8);
canvas.drawText(String.valueOf(i + 1), x, y, mNumPaint);
}
}
if (showLine) {
// 根据行数循环画线
for (int i = lineStart; i < lineEnd; i++) {
int lineY = (i + 1) * lineHeight;
canvas.drawLine(
0,
this.getPaddingTop()
- ViewUtils.getLineSpacingExtra(context,
this) / 2 + lineY,
this.getWidth(),
this.getPaddingTop()
- ViewUtils.getLineSpacingExtra(context,
this) / 2 + lineY, paint);
}
}
} finally {
super.onDraw(canvas);
}
}
// 调整行号的宽度
private void adjustPadding() {
mPaddingLeft = (int) (DisplayUtils.dip2px(context, 15)
+ displayPaddingLeft + mNumberLength
* getNumberTextWidth(mNumPaint));
setPadding(mPaddingLeft, getPaddingTop(), getPaddingRight(),
getPaddingBottom());
}
public void initText(CharSequence text) {
if (!TextUtils.isEmpty(text))
setText(text);
initialized = true;
}
@Override
public void setTextSize(float size) {
Logger.d("mTextSize" + mTextSize);
Logger.d("mTextSize size:" + size);
Logger.d("mTextSize" + Calculator.equals(size, mTextSize));
if (!Calculator.equals(size, mTextSize)) {
super.setTextSize(size);
mTextSize = size;
Logger.d("mTextSize" + mTextSize);
if (showLineNumber) {
mNumPaint.setTextSize(getTextSize());
adjustPadding();
}
Cache.write_float(KEY_TEXT_SIZE, mTextSize);
}
}
public float getTextSizeSP() {
return mTextSize;
}
public static float getNumberTextWidth(Paint paint) {
float[] widths = new float[1];
paint.getTextWidths("0", widths);
return widths[0];
}
public void initNumberLength(String filePath) {
if (showLineNumber) {
KEY_NUMBER_LENGTH = filePath + KEY_NUMBER_LENGTH;
Logger.d("KEY_NUMBER_LENGTH:" + KEY_NUMBER_LENGTH);
mNumberLength = Cache.read_int(KEY_NUMBER_LENGTH,
getNumberLength(getLineCount()));
adjustPadding();
}
}
public static int getTextWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet;
}
private int getNumberLength(int n) {
int length = 0;
while (n > 0) {
n = n / 10;
length++;
}
return length;
}
}
LineSpaceCursorDrawable.java
public class LineSpaceCursorDrawable extends ShapeDrawable {
private Context context;
private EditText view;
public LineSpaceCursorDrawable(Context context, EditText view) {
Logger.d("LineSpaceCursorDrawable new");
this.context = context;
setDither(false);
Resources res = view.getResources();
getPaint().setColor(res.getColor(ThemeManager.getInstance().background_color));//R.color.note_edittext_cursor_color));
setIntrinsicWidth((int)DisplayUtils.dip2px(context, 2));//res.getDimensionPixelSize(R.dimen.detail_notes_text_cursor_width));
this.view = view;
}
public void setBounds(int left, int top, int right, int bottom) {
Logger.d("LineSpaceCursorDrawable setBounds");
Editable s = view.getText();
ImageSpan[] imageSpans = s.getSpans(0, s.length(),
ImageSpan.class);
int selectionStart = view.getSelectionStart();
for (ImageSpan span : imageSpans) {
int start = s.getSpanStart(span);
int end = s.getSpanEnd(span);
if (selectionStart >= start && selectionStart <= end)
{
super.setBounds(left, top, right, top - 1);
return;
}
}
super.setBounds(left, top, right, top + view.getLineHeight() - (int)ViewUtils.getLineSpacingExtra(context, view));
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public static float getLineSpacingExtra(Context context, TextView view){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return view.getLineSpacingExtra();
}
else{
return DisplayUtils.dip2px(context, 8);
}
}
/**
* 将dip或dp值转换为px值,保证尺寸大小不变
*
* @param dipValue
* @param scale
* (DisplayMetrics类中属性density)
* @return
*/
public static float dip2px(Context context, float dipValue) {
float scale = context.getResources().getDisplayMetrics().density;
return dipValue * scale + 0.5f;
}
public class ScreenInfo {
private int widthPixels;
private int heightPixels;
private static ScreenInfo instance;
private ScreenInfo(Activity context) {
DisplayMetrics dm = new DisplayMetrics();
context.getWindowManager().getDefaultDisplay().getMetrics(dm);
this.widthPixels = dm.widthPixels;
this.heightPixels = dm.heightPixels;
}
public static void createInstance(Activity context){
if(instance == null)
instance = new ScreenInfo(context);
}
public static ScreenInfo getInstance(){
return instance;
}
public static ScreenInfo getInstance(Activity context){
if(instance == null)
instance = new ScreenInfo(context);
return instance;
}
/**
* @return the number of pixel in the width of the screen.
*/
public int getWidthPixels() {
return widthPixels;
}
/**
* @return the number of pixel in the height of the screen.
*/
public int getHeightPixels() {
return heightPixels;
}
public String getSize() {
return widthPixels + "×" + heightPixels;
}
}
public class ReflectionUtils {
private static final String TAG = "ReflectionUtils";
public static boolean hasMethod(String className, String method, Class[] params) {
try {
Class<?> targetClass = Class.forName(className);
if (params != null) {
targetClass.getMethod(method, params);
return true;
}
targetClass.getMethod(method, new Class[0]);
return true;
} catch (SecurityException e) {
e.printStackTrace();
return false;
} catch (NoSuchMethodException e2) {
e2.printStackTrace();
return false;
} catch (IllegalArgumentException e3) {
e3.printStackTrace();
return false;
} catch (ClassNotFoundException e4) {
e4.printStackTrace();
return false;
}
}
public static Method getMethod(String className, String method, Class[] params) {
try {
return Class.forName(className).getDeclaredMethod(method, params);
} catch (SecurityException e) {
e.printStackTrace();
return null;
} catch (NoSuchMethodException e2) {
e2.printStackTrace();
return null;
} catch (IllegalArgumentException e3) {
e3.printStackTrace();
return null;
} catch (ClassNotFoundException e4) {
e4.printStackTrace();
return null;
}
}
public static Field getField(String className, String name) {
try {
return Class.forName(className).getDeclaredField(name);
} catch (SecurityException e) {
e.printStackTrace();
return null;
} catch (NoSuchFieldException e2) {
e2.printStackTrace();
return null;
} catch (IllegalArgumentException e3) {
e3.printStackTrace();
return null;
} catch (ClassNotFoundException e4) {
e4.printStackTrace();
return null;
}
}
}
NoteEditText的使用:
<com.xxx.ElasticScrollView
android:id="@+id/scrollview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fastScrollEnabled="true"
android:fillViewport="true" >
<com.xxx.NoteEditText
android:id="@+id/content"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@null"
android:fadingEdge="vertical"
android:gravity="top"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:scrollbars="vertical"
android:lineSpacingExtra="8dp"
android:text="" />
</com.xxx.ElasticScrollView>
ElasticScrollView.java
/**
* 有弹性的ScrollView
* 实现下拉弹回和上拉弹回
* @date Feb 13, 2014 6:11:33 PM
*/
public class ElasticScrollView extends ScrollView {
private static final String TAG = "ElasticScrollView";
//移动因子, 是一个百分比, 比如手指移动了100px, 那么View就只移动50px
//目的是达到一个延迟的效果
private static final float MOVE_FACTOR = 0.2f;
//松开手指后, 界面回到正常位置需要的动画时间
private static final int ANIM_TIME = 300;
//ScrollView的子View, 也是ScrollView的唯一一个子View
private View contentView;
//手指按下时的Y值, 用于在移动时计算移动距离
//如果按下时不能上拉和下拉, 会在手指移动时更新为当前手指的Y值
private float startY;
//用于记录正常的布局位置
private Rect originalRect = new Rect();
//手指按下时记录是否可以继续下拉
private boolean canPullDown = false;
//手指按下时记录是否可以继续上拉
private boolean canPullUp = false;
//在手指滑动的过程中记录是否移动了布局
private boolean isMoved = false;
public ElasticScrollView(Context context) {
super(context);
}
public ElasticScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
contentView = getChildAt(0);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(contentView == null) return;
//ScrollView中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变
originalRect.set(contentView.getLeft(), contentView.getTop(), contentView
.getRight(), contentView.getBottom());
}
/**
* 在该方法中获取ScrollView中的唯一子控件的位置信息
* 这个位置信息在整个控件的生命周期中保持不变
*/
/**
* 在触摸事件中, 处理上拉和下拉的逻辑
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (contentView == null) {
return super.dispatchTouchEvent(ev);
}
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//判断是否可以上拉和下拉
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
//记录按下时的Y值
startY = ev.getY();
break;
case MotionEvent.ACTION_UP:
if(!isMoved) break; //如果没有移动布局, 则跳过执行
// 开启动画
TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(),
originalRect.top);
anim.setDuration(ANIM_TIME);
contentView.startAnimation(anim);
// 设置回到正常的布局位置
contentView.layout(originalRect.left, originalRect.top,
originalRect.right, originalRect.bottom);
//将标志位设回false
canPullDown = false;
canPullUp = false;
isMoved = false;
break;
case MotionEvent.ACTION_MOVE:
//在移动的过程中, 既没有滚动到可以上拉的程度, 也没有滚动到可以下拉的程度
if(!canPullDown && !canPullUp) {
startY = ev.getY();
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
break;
}
//计算手指移动的距离
float nowY = ev.getY();
int deltaY = (int) (nowY - startY);
//是否应该移动布局
boolean shouldMove =
(canPullDown && deltaY > 0) //可以下拉, 并且手指向下移动
|| (canPullUp && deltaY< 0) //可以上拉, 并且手指向上移动
|| (canPullUp && canPullDown); //既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小)
if(shouldMove){
//计算偏移量
int offset = (int)(deltaY * MOVE_FACTOR);
//随着手指的移动而移动布局
contentView.layout(originalRect.left, originalRect.top + offset,
originalRect.right, originalRect.bottom + offset);
isMoved = true; //记录移动了布局
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 判断是否滚动到顶部
*/
private boolean isCanPullDown() {
return getScrollY() == 0 ||
contentView.getHeight() < getHeight() + getScrollY();
}
/**
* 判断是否滚动到底部
*/
private boolean isCanPullUp() {
return contentView.getHeight() <= getHeight() + getScrollY();
}
}
((NoteEditText) editText).setScrollView((ScrollView) view
.findViewById(R.id.scrollview));
效果展示,顺便打个广告: