很久没有更新,今天为大家带来我在项目中利用ReplacementSpan来打造纯文本的填空题分享。效果如图

1.认识ReplacementSpan

关于ReplacementSpan这个类,开发者文档和源码里没明确说明。不过通过名字我们大概可以猜出来,它是替换Span的,而Span是替换文本里的文字,进行自定义属性的,既然是替换,所以ReplacementSpan也有这个功能。那ReplacementSpan是通过什么来让我们自定义属性的呢?我们新建工程,创建一个新的类继承ReplacementSpan,会发现有两个方法需要我们Override:

/*** Returns the width of the span*/
public abstract int getSize(Paint paint, CharSequence   text, int start, int end, Paint.FontMetricsInt fm);/*** Draws the span into the canvas.*/
public abstract void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint);————————————————

第一个方法  getSize,顾名思义,它是计算我们替换文字所需要的宽度

第二个方法  和自定义视图onDraw方法一样,是在TextView绘制时调用这个方法,因此我们自定义属性就是在这个方法里完成的

2.自定义ReplacementSpan

新建类,继承ReplacementSpan,实现getSize和draw方法,然后定义

/*** * @param isSelect 空是否选中* @param context  上下文* @param userFillString 用户填入的字段* @param lineSpacing tv的行间距*/public FillReplaceSpan(boolean isSelect, Context context,String userFillString,float lineSpacing) {this.isSelect = isSelect;this.context = context;this.mText=userFillString;this.lineSpacing=lineSpacing;}

然后重写getSize

@Overridepublic int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {String mSWidth=mText;mWidth = (int) paint.measureText(mSWidth, 0, mSWidth.length());int defaultWidth = 120;//默认下划线长度if (mWidth< defaultWidth){mWidth= defaultWidth;}return mWidth;}

最后在draw中绘制下划线以及文本

@Overridepublic void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {//填入对应单词int width = (int) paint.measureText(mText, 0, mText.length());width = (mWidth - width) / 2;if (isSelect) {paint.setStyle(Paint.Style.FILL);paint.setColor(ContextCompat.getColor(context, R.color.colorAccent));} else {paint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));}paint.setStrokeWidth(5);//绘制下划线if (count - 1 == id) {canvas.drawLine(x, bottom - lineSpacing / 2, x + mWidth, bottom - lineSpacing / 2, paint);} else {canvas.drawLine(x, bottom - lineSpacing, x + mWidth, bottom - lineSpacing, paint);}if (!TextUtils.isEmpty(mText)) {paint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));canvas.drawText(mText, 0, mText.length(), x + width, (float) y, paint);}}

完整ReplacementSpan代码

/*** 自定义的Span,用来绘制填空题*/
public class FillReplaceSpan extends ReplacementSpan {private boolean isSelect;private Context context;private int index;private int count=10;//总共的空数private float lineSpacing;//文本的行间距/**** @param isSelect 空是否选中* @param context  上下文* @param userFillString 用户填入的字段* @param lineSpacing tv的行间距*/public FillReplaceSpan(boolean isSelect, Context context,String userFillString,float lineSpacing) {this.isSelect = isSelect;this.context = context;this.mText=userFillString;this.lineSpacing=lineSpacing;}public void setmOnSelect(OnSelect mOnSelect) {this.mOnSelect = mOnSelect;}public  interface OnClick {void onClick(TextView v, int id, FillReplaceSpan span);}public  interface OnSelect {void onSelect(TextView v, Spannable buffer, int id, FillReplaceSpan span);}public int id = 0;//回调中的对应Span的IDprivate int mWidth = 0;//最长单词的宽度
//    public String mWidthStr;//对应句子最长的单词public String mText;//保存的Stringpublic Object mObject;//回调中的任意对象public OnClick mOnClick;private OnSelect mOnSelect;//    public void setWidth(String widthStr) {mWidthStr = widthStr;
//        mWidth = 0;
//    }public void onClick(TextView v, Spannable buffer, boolean isDown, int x, int y, int line, int off) {if (mOnClick != null) {mOnClick.onClick(v, id, this);}if (mOnSelect != null) {mOnSelect.onSelect(v, buffer, id, this);}}@Overridepublic int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {//将返回相对于Paint画笔的文本 50== 左右两边增加的空余长度String mSWidth=mText;mWidth = (int) paint.measureText(mSWidth, 0, mSWidth.length());int defaultWidth = 120;//默认下划线长度if (mWidth< defaultWidth){mWidth= defaultWidth;}return mWidth;}@Overridepublic void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {//填入对应单词int width = (int) paint.measureText(mText, 0, mText.length());width = (mWidth - width) / 2;if (isSelect) {paint.setStyle(Paint.Style.FILL);paint.setColor(ContextCompat.getColor(context, R.color.colorAccent));} else {paint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));}paint.setStrokeWidth(5);//绘制下划线if (count - 1 == id) {canvas.drawLine(x, bottom - lineSpacing / 2, x + mWidth, bottom - lineSpacing / 2, paint);} else {canvas.drawLine(x, bottom - lineSpacing, x + mWidth, bottom - lineSpacing, paint);}if (!TextUtils.isEmpty(mText)) {paint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));canvas.drawText(mText, 0, mText.length(), x + width, (float) y, paint);}}//TextView触摸事件-->Span点击事件public  static LinkMovementMethod Method = new LinkMovementMethod() {public boolean onTouchEvent(TextView widget, Spannable buffer,MotionEvent event) {int action = event.getAction();if (action == MotionEvent.ACTION_UP ||action == MotionEvent.ACTION_DOWN) {int x = (int) event.getX();int y = (int) event.getY();x -= widget.getTotalPaddingLeft();y -= widget.getTotalPaddingTop();x += widget.getScrollX();y += widget.getScrollY();Layout layout = widget.getLayout();int line = layout.getLineForVertical(y);int off = layout.getOffsetForHorizontal(line, x);FillReplaceSpan[] link = buffer.getSpans(off, off, FillReplaceSpan.class);if (link.length != 0) {//Span的点击事件if (action == MotionEvent.ACTION_UP) {link[0].onClick(widget, buffer, false, x, y, line, off);} else if (action == MotionEvent.ACTION_DOWN) {link[0].onClick(widget, buffer, true, x, y, line, off);
//                        Selection.setSelection(buffer,
//                                buffer.getSpanStart(link[0]),
//                                buffer.getSpanEnd(link[0]));}return true;}  //                    Selection.removeSelection(buffer);}return false;}};public void setSelect(boolean select) {this.isSelect = select;}

这样我们ReplacementSpan我们就完成了,但这只是一个视图,所以我们需要一个管理类来管理这个ReplacementSpan

3.创建FillSpanController管理类

这个类是用来处理数据以及和ReplacementSpan交互

创建makeData方法,用来处理数据以及插入ReplacementSpan

//造对应的sentencepublic void makeData(TextView tv, String str, FillReplaceSpan.OnClick onClick){if (tv == null || TextUtils.isEmpty(str))return;try{tv.setMovementMethod(FillReplaceSpan.Method);mTv = tv;char[] chars = str.toCharArray();for (int i = 0; i < chars.length; i++) {//获取需要绘制空的坐标位置if ('['==chars[i]){mListIndex.add(i-mListIndex.size());}else if(']'==chars[i]){mListIndex.add(i-mListIndex.size());}}mStr = str.replace("[","").replace("]","");mSpanString = new SpannableString(mStr);int index = 0;for (int i = 0; i < mListIndex.size(); i+=2) {FillReplaceSpan span = new FillReplaceSpan(i == 0,context,"",tv.getLineSpacingExtra());span.mOnClick = onClick;span.id = index++;mSpans.add(span);mSpanString.setSpan(span, mListIndex.get(i), mListIndex.get(i+1), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);}}catch (Exception e){e.printStackTrace();}tv.setText(mSpanString);}

数据处理完成了,空也画出来了,但是还无法输入,要想要弹出软键盘以及接收到键盘的输入,那么我们就需要一个EditText,但是EditText我们放在哪里呢,当然是用户所选空的位置,因此我们在FillSpanController写一个获取ReplacementSpan位置

以及设置EditText位置的方法

//获取出对应Span的RectF数据public RectF drawSpanRect(TextView v, FillReplaceSpan s) {Layout layout = v.getLayout();Spannable buffer = (Spannable) v.getText();int l = buffer.getSpanStart(s);int r = buffer.getSpanEnd(s);int line = layout.getLineForOffset(l);int l2 = layout.getLineForOffset(r);if (mRf == null){mRf = new RectF();Rect rt = new Rect();v.getPaint().getTextBounds("TgQyYjJ",0,7,rt);mFontT = rt.top;mFontB  = rt.bottom;}mRf.left = layout.getPrimaryHorizontal(l);mRf.right = layout.getSecondaryHorizontal(r);// 通过基线去校准line = layout.getLineBaseline(line);mRf.top = line + mFontT;mRf.bottom = line + mFontB;return mRf;}//设置EditText填空题中的相对位置public void setEtXY(TextView tv, EditText et, RectF rf) {//设置et w,h的值RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) et.getLayoutParams();lp.width = (int)(rf.right - rf.left);lp.height = (int)(rf.bottom - rf.top);//设置et 相对于tv x,y的相对位置lp.leftMargin = (int) (tv.getLeft()+rf.left);lp.topMargin  = (int) (tv.getTop()+rf.top);et.setLayoutParams(lp);
//        获取焦点,弹出软键盘et.setFocusable(true);et.requestFocus();showImm(true,et);}

这样我们打造完成了,FillSpanController完整代码

/*** 填空题的控制器*/
public class FillSpanController {private TextView mTv;private SpannableString mSpanString;private int mFontT; // 字体topprivate int mFontB;// 字体bottompublic int mOldSpan = -1;private String mStr;public String mWidthStr;private ArrayList<Integer> mListIndex = new ArrayList<Integer>();private ArrayList<FillReplaceSpan> mSpans = new ArrayList<>();protected ImmFocus mFocus = new ImmFocus();private RectF mRf;private Context context;public FillSpanController(Context context) {this.context=context;}//造对应的sentencepublic void makeData(TextView tv, String str, FillReplaceSpan.OnClick onClick){if (tv == null || TextUtils.isEmpty(str))return;try{tv.setMovementMethod(FillReplaceSpan.Method);mTv = tv;char[] chars = str.toCharArray();for (int i = 0; i < chars.length; i++) {//正常情况下要去掉'[',']',减掉mListIndex.size()if ('['==chars[i]){mListIndex.add(i-mListIndex.size());}else if(']'==chars[i]){mListIndex.add(i-mListIndex.size());}}mStr = str.replace("[","").replace("]","");mSpanString = new SpannableString(mStr);int index = 0;for (int i = 0; i < mListIndex.size(); i+=2) {FillReplaceSpan span = new FillReplaceSpan(i == 0,context,"",tv.getLineSpacingExtra());span.mOnClick = onClick;span.id = index++;mSpans.add(span);mSpanString.setSpan(span, mListIndex.get(i), mListIndex.get(i+1), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);}}catch (Exception e){e.printStackTrace();}tv.setText(mSpanString);}//填充缓存的数据public void setData(String str, int i){if (mTv == null || mSpans ==null ||mSpans.size() ==0 || i<0 ||i>mSpans.size()-1)return;FillReplaceSpan span = mSpans.get(i);span.mText = str;mTv.setText(mSpanString);}public void setData(int i){for (int z=0;z<mSpans.size();z++){if (mSpans.get(z).id==i){mSpans.get(z).setSelect(true);}else{mSpans.get(z).setSelect(false);}}mTv.invalidate();}public int setData(String str, Object o){if (mTv == null)return -2;for (int i = 0; i < mSpans.size(); i++) {FillReplaceSpan span = mSpans.get(i);if (TextUtils.isEmpty(span.mText)){span.mText = str;span.id = i;span.mObject = o;mTv.invalidate();return i;}}//-1说明填空题已经填满return -1;}public int isFill(){for (int i = 0; i < mSpans.size(); i++) {FillReplaceSpan span = mSpans.get(i);if (TextUtils.isEmpty(span.mText)){return i;}}//-1说明填空题已经填满return -1;}//获取出对应Span的RectF数据public RectF drawSpanRect(TextView v, FillReplaceSpan s) {Layout layout = v.getLayout();Spannable buffer = (Spannable) v.getText();int l = buffer.getSpanStart(s);int r = buffer.getSpanEnd(s);int line = layout.getLineForOffset(l);int l2 = layout.getLineForOffset(r);if (mRf == null){mRf = new RectF();Rect rt = new Rect();v.getPaint().getTextBounds("TgQyYjJ",0,7,rt);mFontT = rt.top;mFontB  = rt.bottom;}mRf.left = layout.getPrimaryHorizontal(l);mRf.right = layout.getSecondaryHorizontal(r);// 通过基线去校准line = layout.getLineBaseline(line);mRf.top = line + mFontT;mRf.bottom = line + mFontB;return mRf;}//设置EditText填空题中的相对位置public void setEtXY(TextView tv, EditText et, RectF rf) {//设置et w,h的值RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) et.getLayoutParams();lp.width = (int)(rf.right - rf.left);lp.height = (int)(rf.bottom - rf.top);//设置et 相对于tv x,y的相对位置lp.leftMargin = (int) (tv.getLeft()+rf.left);lp.topMargin  = (int) (tv.getTop()+rf.top);et.setLayoutParams(lp);
//        获取焦点,弹出软键盘et.setFocusable(true);et.requestFocus();showImm(true,et);}public void clearData(){for (FillReplaceSpan replaceSpan:mSpans){replaceSpan.mText="";}mTv.setText(mSpanString);}public String getAllAnswer(){StringBuffer sb = new StringBuffer();for (int i = 0; i < mSpans.size(); i++) {FillReplaceSpan span = mSpans.get(i);if(i == mSpans.size() -1){sb.append(span.mText);}else{sb.append(span.mText).append(",");}}return sb.toString();}public ArrayList<FillReplaceSpan> getSpanAll(){return mSpans;}public void showImm(boolean bOn, View focus) {try {if (bOn) {if (focus!=null) {ImmFocus.show(true, focus);} else {mFocus.setFocus(focus);}} else {ImmFocus.show(false, null);}}catch (Exception e){e.printStackTrace();}}}

4.使用FillSpanController

public class MainActivity extends AppCompatActivity implements FillReplaceSpan.OnClick {private EditText editText;//提供弹出键盘的输入框private TextView textView;private int spanPosition=-1;//填空题空的位置private FillSpanController fillSpanController;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();initData();}private void initView(){editText = findViewById(R.id.fill_span_edit);textView = findViewById(R.id.fill_span_text);editText.addTextChangedListener(new TextWatcher() {@Overridepublic void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}@Overridepublic void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {fillSpanController.setData(charSequence.toString(),spanPosition);}@Overridepublic void afterTextChanged(Editable editable) {}});}private void initData(){String fillString="Two people died.\n" +"Roads and highways are closed.\n" +"Homes are without power.\n" +"And travelers are (1)_______ in Great Britain because of record rainfall.\n" +"The rain set records and affected the northern part of England and Scotland.\n" +"The national weather service (2)_______ a \"red\" alert for rain in the area.\n" +"In some areas, water reached above the doors of parked cars.\n" +"Rescue workers removed (3)______________ by boat from a flooded residential street in Carlisle, Britain December six.\n" +"The Reuters news agency says two people died because of the flooding.\n" +"The head of Britain's Environment Agency called the weather unprecedented.\"\n" +"Most of the (4)________ received between 200 to 300 millimeters of rain over the weekend,(5)______________ the U.K.'s National Weather Service.\n" +"The weather office says (6)_________ weather may continue into the week.(7)___________ rainy weather is not only affecting the northern hemisphere.\n" +"The city of Chennai in southern India also received over 300 millimeters of rain in 24 hours last week.\n" +"The rain (8)_____________ came at the same time world leaders are meeting in Franceto (9)__________ climate change at the COP21 convention.\n" +"One climate change expert from the United Kingdom's office of the World Wildlife Fund said \"storm Desmond is the sort of storm that we will see more of if we fail to (10)________ climate change.";while (fillString.contains("___")){fillString=fillString.replace("___","__");//将数据所要画空的地方统一}fillString=fillString.replace("__","[A]");//将画空数据替换成我们需要替换的某块 [A]只是一个替换判断值,可修改FillSpanController.makeData里的判断元素自己进行替换fillSpanController = new FillSpanController(getApplicationContext());fillSpanController.makeData(textView,fillString,this);}//Span点击回调@Overridepublic void onClick(TextView v, int id, FillReplaceSpan span) {if (id!=spanPosition){fillSpanController.setData(id);fillSpanController.setEtXY(textView,editText,fillSpanController.drawSpanRect(textView,span));spanPosition=id;editText.setText("");}}
}

到此就大功告成了。当然目前填入功能还不完善,我将在下一篇中完善:保存输入的答案以及判断对错。

补充:ImmFocus类

// 处理焦点
public class ImmFocus {// 处理编辑框焦点及输入法protected View mLastFocus;// 保存焦点public void save(View focus) {mLastFocus = focus;if (mLastFocus!=null&&(!show(false,mLastFocus)||!(mLastFocus instanceof TextView))) mLastFocus = null;}// 恢复焦点public void restore() {if (mLastFocus!=null) {show(true,mLastFocus);mLastFocus = null;}}// 预约焦点public void setFocus(View focus) {mLastFocus = focus;}// 显示/隐藏public static boolean show(boolean bOn,View focus) {InputMethodManager imm = (InputMethodManager)focus.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);if (bOn) {focus.requestFocus();return imm.showSoftInput(focus, 0);} else {return imm.hideSoftInputFromWindow(focus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);}}
}

5.源码下载

源码下载https://download.csdn.net/download/pengguichu/11620663https://download.csdn.net/download/pengguichu/11620663

Android 利用ReplacementSpan打造纯文本填空题(附源码)相关推荐

  1. Qt利用avilib实现录屏功能_openlayers6结合geoserver利用WFS服务实现图层编辑功能(附源码下载)...

    内容概览 1.openlayers6结合geoserver利用WFS服务实现图层编辑功能 2.源代码demo下载 效果图如下: 本篇主要是参照openlayers6结合geoserver利用WFS服务 ...

  2. arcgis xml 下载 切片_openlayers6结合geoserver利用WFS服务实现图层编辑功能(附源码下载)...

    内容概览 1.openlayers6结合geoserver利用WFS服务实现图层编辑功能 2.源代码demo下载 效果图如下: 本篇主要是参照openlayers6结合geoserver利用WFS服务 ...

  3. Android悬浮窗开启 适配所有机型(附源码)

    Android悬浮窗开启 适配所有机型(附源码) 1.开启悬浮窗权限 清单文件中添加: <uses-permission android:name="android.permissio ...

  4. AOP注解@Before、@AfterReturning拦截单个方法的入参和出参,纯注解方式(附源码下载),解决单个方法不生效问题(一)

    AOP注解@Before.@AfterReturning拦截单个方法的入参和出参,纯注解方式(附源码下载),解决单个方法不生效问题(一) 问题背景 AOP注解@Before.@AfterReturni ...

  5. 服务器端配置正方教务系统,手把手带你打造一个教务系统客户端(附源码)

    本篇博客主要和大家分享编写一个学校教务系统的客户端版本,主要是关于登录以及数据获取方面,结尾还会附上本人以前编写的客户端源代码,有兴趣的可以自行下载玩耍~ 阅读本文大概需要5分钟. 前言 好久没有更新 ...

  6. springboot 古诗文学习系统【纯干货分享,附源码91747】

    摘  要 随着科学技术的飞速发展,社会的方方面面.各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,古诗文学习系统当然也不能排除在外.古诗文学习系统是以实际运用为开发背景,运用软件工 ...

  7. 基于flask徐州市天气信息可视化分析系统【纯干货分享,附源码04600】

    摘 要 信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题.针对天气信息等问题,对天气 ...

  8. Android使用xml自定义软键盘效果(附源码)

    Android使用xml自定义软键盘效果原理: 1,软键盘其实是个控件,使用android.inputmethodserver.KeyboardView类定义. 2,主布局中使用帧布局,当我们需要显示 ...

  9. 【Pytorch】利用Pytorch+GRU实现情感分类(附源码)

    在这个实验中,数据的预处理过程以及网络的初始化及模型的训练等过程同前文<利用Pytorch+LSTM实现中文新闻分类>,具体这里就不再重复解释了.如果有读者在对数据集的预处理过程中有疑问, ...

最新文章

  1. 最新 macOS Sierra 10.12.3 安装CocoaPods及使用详解
  2. Coding-排序(sort)
  3. 网络服务-VSFTP
  4. Windows中文件夹属性加密的作用?
  5. 打破你的认知,数字除以 0 一定会崩溃吗?
  6. 解决kettle配置文件中的中文乱码
  7. 【汇编语言】状态标志符(CF/OF/SF/ZF)在运算(ADD/SUB/ADC/SBB)过程中的响应变化
  8. countdownlatch的使用详解(好懂!!)
  9. 苹果紧急修复已遭利用的两个0day
  10. iOS 使用mp4v2合成的视频注意事项
  11. 代码保护软件 VMProtect 用户手册: 什么是VMProtect?
  12. 软件设计模式学习总结
  13. shell脚本:编辑脚本check_host.sh,自动检测主机如下信息
  14. 橙光游戏软件 怎么整体测试,橙光游戏怎么让编辑来审核?
  15. 学习mysql比较好一些书籍
  16. 手机上最好用的五笔输入法_最欠揍的手机输入法,用不好失业又失恋
  17. 干货分享 | 最新机器学习视频教程与数据集下载(持续更新......)
  18. 代码风格自动化(二)——husky + lint-staged
  19. 如何听广播来学计算机,MAC使用技巧之苹果itunes如何收听国内的广播?
  20. 阿里云对象存储OSS(Object Storage Service)

热门文章

  1. Oracle 12c 自带的SQL Developer新建连接出现的问题:Got minus one from a read call,connect lapse 60018 ms,....(已解决)
  2. 2023华为od机试真题【简易内存池】C语言
  3. 外媒称苹果iPhone7发布时间将提前
  4. JavaScript在数组中寻找相同对象元素的问题
  5. 计算机等级考试嵌入式三级重点考点归纳
  6. 对古人“一命二运三风水,四积德五读书”的人生命运总结的理解
  7. chown: changing ownership of ‘ypj’: Operation not permitted
  8. c# js popup_关于WPF中Popup中的一些用法的总结
  9. 自定义开发popup点击非popup区域关闭popup
  10. Linux脚本csh 遇到 Badly placed ()'s 的问题。