最近因项目需求,使用到CloudEditText 来实现文字输入,并且需要点击改变ImageSpan背景,使用软键盘进行删除操作
先说明一下原理,CloudEditText 是使用 SpannableString 来进行插入带有样式的文字,主要分3层:
1.SpannableString 必须有字符串传入,不然后续的插入ImageSpan 与 ClickableSpan 都会出现数组越界问题,因为没有字符串的插入,EditText本身就是空的
SpannableString spannableString=new SpannableString(getText())
2.ImageSpan 插入drawable 到 对应字符串长度的区间
spannableString.setSpan(imageSpan,start,end,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
3.ClickableSpan 插入同ImageSpan 相同的区间
spannableString.setSpan(clickSpan,start,end,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
想要ClickableSpan 触发点击事件,edittext.setMovementMethod(LinkMovementMethod.getInstance()) 这句话是必须要加的
说说这里边会遇到的一些坑,
1.点击事件位置偏移问题
问题主要出在LinkMovementMethod 这个类中的OnTouchEvent 方法中,源码里边是这样写的 int off =layout.getOffsetForHorizontal(line,x) ,经查阅此方法返回值是最接近手指触摸位置的偏移量,这里就会出现一个问题,当手指触摸到某个字符最前边位置时,光标会选中在字符前边,此时去删除的话,必然会删掉前一个字符。
之后在API中找到根据位置获取x轴位移的方法,layout.getPrimaryHorizontal(off) ,解决方案如下:
float xLeft=layout.getPrimaryHorizontal(off); if(xLeft<x){ off+=1; }else{ off-=1; } ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
2.点击EditText空白区域会选中最后一个span,这个简单,判断用户触摸位置在行宽之后,不去触发ClickableSpan的点击事件就好了
if(x<layout.getLineWidth(line)&&x>0){ link[0].onClick(widget); }
3.Nexus系列原生键盘无法正确使用删除键(View.OnKeyListener 中监听删除键,原则上span 禁用系统删除,字符让系统处理)
@Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { return new BackInputConnection(super.onCreateInputConnection(outAttrs),true); } private class BackInputConnection extends InputConnectionWrapper { public BackInputConnection(InputConnection target, boolean mutable) { super(target, mutable); } @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { if (beforeLength == 1 && afterLength == 0) { return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); } return super.deleteSurroundingText(beforeLength, afterLength); } }
4.textMultiLine 多行时,软键盘无法响应action done (TextView.OnEditorActionListener中监听)
@Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { InputConnection connection=super.onCreateInputConnection(outAttrs); int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION; if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) { outAttrs.imeOptions ^= imeActions;// clear the existing action outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;// set the done action } if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; } return new GoogleInputConnection(connection,true); }
5.setSelection(index) 方法无效
this.post(new Runnable() { @Override public void run() { setSelection(getText().length()); } });
6.imagespan 无法居中,只能有文字下边缘对齐(重写imagespan)
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt) { Drawable drawable = getDrawable(); Rect rect = drawable.getBounds(); if (fontMetricsInt != null) { Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt(); int fontHeight = fmPaint.bottom - fmPaint.top; int drHeight = rect.bottom - rect.top; int top = drHeight / 2 - fontHeight / 4; int bottom = drHeight / 2 + fontHeight / 4; fontMetricsInt.ascent = -bottom; fontMetricsInt.top = -bottom; fontMetricsInt.bottom = top; fontMetricsInt.descent = top; } return rect.right; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { Drawable drawable = getDrawable(); canvas.save(); int transY = 0; transY = ((bottom - top) - drawable.getBounds().bottom) / 2 + top; canvas.translate(x, transY); drawable.draw(canvas); canvas.restore(); }
7.imagespan 中内容超出屏幕宽度,会出现2个重复span
str 为imagespan 内容字符
screenWidth 为屏幕宽度
使用TextUtils.ellipsize()方法格式化 内容部分 超出屏幕部分以...替换
TextUtils.ellipsize(str, getPaint(),screenWidth, TextUtils.TruncateAt.END).toString()
以下为关键代码片段
1. 添加一个span (isClick 在点击切换背景时使用)
public void insertSpan(final String str, boolean isClick){ getText().append(str); SpannableStringBuilder spannableString=new SpannableStringBuilder(getText()); View spanView = getSpanView(getContext(), str,isClick); BitmapDrawable bitmpaDrawable = (BitmapDrawable) UIUtils.convertViewToDrawable(spanView); bitmpaDrawable.setBounds(0, 0, bitmpaDrawable.getIntrinsicWidth(), bitmpaDrawable.getIntrinsicHeight()); ClickableImageSpan imageSpan= new ClickableImageSpan(bitmpaDrawable); int len=getText().toString().length(); int end=len; int start=len-str.length(); imageSpan.setText(str); imageSpan.setStart(start); imageSpan.setEnd(end); spannableString.setSpan(imageSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ClickableSpan clickableSpan=new ClickableSpan() { @Override public void onClick(View widget) { judgeLastTextIsEmail(str); } }; spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); setText(spannableString); setSelection(spannableString.length()); }
public View getSpanView(Context context,String text,boolean isClick){ final TextView view = new TextView(context); FrameLayout.LayoutParams params=new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,FrameLayout.LayoutParams.WRAP_CONTENT); view.setLayoutParams(params); view.setPadding(UIUtils.dip2px(context, 10), 0, UIUtils.dip2px(context, 10), 0); view.setText(text); view.setSingleLine(true); view.setTextSize(getTextSize()); if(isClick){ view.setBackgroundResource(R.drawable.edittext_span_back); }else{ view.setBackgroundResource(R.drawable.edittext_span_non_back); } view.setTextColor(getCurrentTextColor()); FrameLayout frameLayout=new FrameLayout(context); ViewGroup.LayoutParams frameParams=new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); frameLayout.setLayoutParams(frameParams); frameLayout.addView(view); return frameLayout; }
public class UIUtils { private final static int UPPER_LEFT_X = 0; private final static int UPPER_LEFT_Y = 0; public static Drawable convertViewToDrawable(View view) { int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); view.measure(spec, spec); view.layout(UPPER_LEFT_X, UPPER_LEFT_Y, view.getMeasuredWidth(), view.getMeasuredHeight()); Bitmap b = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); c.translate(-view.getScrollX(), -view.getScrollY()); view.draw(c); view.setDrawingCacheEnabled(true); Bitmap cacheBmp = view.getDrawingCache(); Bitmap viewBmp = cacheBmp.copy(Bitmap.Config.ARGB_8888, true); cacheBmp.recycle(); view.destroyDrawingCache(); return new BitmapDrawable(viewBmp); } public static int dip2px(Context context,int dip) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dip * scale + 0.5f); } public static int px2sp(Context context,float pxValue) { final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; return (int) (pxValue / fontScale + 0.5f); } public static int sp2px(Context context,float spValue) { final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; return (int) (spValue * fontScale + 0.5f); } /** * hide soft keyboard */ public static void hideSoftKeyboard(Activity activity, View view) { InputMethodManager imm = (InputMethodManager) activity .getSystemService(Activity.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } /** * show soft keyboard */ public static void showSoftKeyboard(Activity activity, View view) { InputMethodManager imm = (InputMethodManager) activity .getSystemService(Activity.INPUT_METHOD_SERVICE); imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); }