Android使用代码实现一个填空题

龙旋

共 8341字,需浏览 17分钟

 ·

2021-08-18 18:12

刚看到要做填空题这个需求的时候,第一个反应是到百度,啊...不对,谷歌上搜一下有没有类似的Demo,无奈搜出来的全是Android面试题,唉,算了,还是老老实实自己实现吧,先看下效果:


学习一些基础知识


首先来学习一下如何对TextView的局部设置颜色和点击事件,这里要用到一个很重要的类SpannableString。
Talk is cheap. Show me the code.
public class SpannableStringActivity extends BaseActivity {
@Bind(R.id.tv_content) TextView tvContent;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_spannable_string); ButterKnife.bind(this);
initData(); }
private void initData() { String originContent = "你看我不仅能变颜色,还能点击。"; SpannableString content = new SpannableString(originContent);
// 设置颜色 ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#4DB6AC")); content.setSpan(colorSpan, 7, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置点击事件 MyClickableSpan myClickableSpan = new MyClickableSpan(); content.setSpan(myClickableSpan, 12, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // 设置此方法后,点击事件才能生效 tvContent.setMovementMethod(LinkMovementMethod.getInstance());
tvContent.setText(content); }
class MyClickableSpan extends ClickableSpan {
@Override public void onClick(View widget) { Toast.makeText(SpannableStringActivity.this, "我被点击了", Toast.LENGTH_SHORT).show(); } }}

看下效果:


简单说下,首先把要显示的内容转成SpannableString对象,然后通过ForegroundColorSpan设置颜色,ClickableSpan设置点击事件,SpannableString通过调用setSpan方法将颜色和点击事件应用到显示的内容中,setSpan方法需要传入设置格式生效的起始坐标(比如颜色的起始坐标分别是7、8,那么就传入7,8+1),最后注意一下Spanned.SPAN_EXCLUSIVE_EXCLUSIVE这个标志,一共有四种flag可以选择,分别是:

  • Spanned.SPAN_INCLUSIVE_INCLUSIVE:前后都包括

  • Spanned.SPAN_EXCLUSIVE_EXCLUSIVE:前后都不包括

  • Spanned.SPAN_INCLUSIVE_EXCLUSIVE:前面包括,后面不包括

  • Spanned.SPAN_EXCLUSIVE_INCLUSIVE:前面不包括,后面包括


这个flag是用来标识在Span范围内的文本,前后输入新的字符时是否也使用这个效果用的。一脸蒙圈,啥,你说的是啥?还是看图吧:

我们把flag设置为:
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
(前面包括,后面不包括)。



是不是清晰了很多,如果图还看不懂,慢走不送!

实现


首先初始化一些数据

public class FillBlankView extends RelativeLayout {
private TextView tvContent; private Context context; // 答案集合 private List<String> answerList; // 答案范围集合 private List<AnswerRange> rangeList; // 填空题内容 private SpannableStringBuilder content;
public FillBlankView(Context context) { this(context, null); }
public FillBlankView(Context context, AttributeSet attrs) { this(context, attrs, 0); }
public FillBlankView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; initView(); }
private void initView() { LayoutInflater inflater = LayoutInflater.from(context); inflater.inflate(R.layout.layout_fill_blank, this);
tvContent = (TextView) findViewById(R.id.tv_content); }
...}

定义一个设置数据的方法,供外部调用
/** * 设置数据 * * @param originContent   源数据 * @param answerRangeList 答案范围集合 */public void setData(String originContent, List<AnswerRange> answerRangeList) {    if (TextUtils.isEmpty(originContent) || answerRangeList == null            || answerRangeList.isEmpty()) {        return;    }
// 获取课文内容 content = new SpannableStringBuilder(originContent); // 答案范围集合 rangeList = answerRangeList;
// 设置下划线颜色 for (AnswerRange range : rangeList) { ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#4DB6AC")); content.setSpan(colorSpan, range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); }
// 答案集合 answerList = new ArrayList<>(); for (int i = 0; i < rangeList.size(); i++) { answerList.add(""); }
// 设置填空处点击事件 for (int i = 0; i < rangeList.size(); i++) { AnswerRange range = rangeList.get(i); BlankClickableSpan blankClickableSpan = new BlankClickableSpan(i); content.setSpan(blankClickableSpan, range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); }
// 设置此方法后,点击事件才能生效 tvContent.setMovementMethod(LinkMovementMethod.getInstance()); tvContent.setText(content);}

代码中已经写了很全的注释,主要是设置填空处的颜色和点击事件。

点击事件
/** * 点击事件 */class BlankClickableSpan extends ClickableSpan {
private int position;
public BlankClickableSpan(int position) { this.position = position; }
@Override public void onClick(final View widget) { View view = LayoutInflater.from(context).inflate(R.layout.layout_input, null); final EditText etInput = (EditText) view.findViewById(R.id.et_answer); Button btnFillBlank = (Button) view.findViewById(R.id.btn_fill_blank);
// 显示原有答案 String oldAnswer = answerList.get(position); if (!TextUtils.isEmpty(oldAnswer)) { etInput.setText(oldAnswer); etInput.setSelection(oldAnswer.length()); }
final PopupWindow popupWindow = new PopupWindow(view, LayoutParams.MATCH_PARENT, dp2px(40)); // 获取焦点 popupWindow.setFocusable(true); // 为了防止弹出菜单获取焦点之后,点击Activity的其他组件没有响应 popupWindow.setBackgroundDrawable(new PaintDrawable()); // 设置PopupWindow在软键盘的上方 popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); // 弹出PopupWindow popupWindow.showAtLocation(tvContent, Gravity.BOTTOM, 0, 0);
btnFillBlank.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 填写答案 String answer = etInput.getText().toString(); fillAnswer(answer, position); popupWindow.dismiss(); } });
// 显示软键盘 InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); }
@Override public void updateDrawState(TextPaint ds) { // 不显示下划线 ds.setUnderlineText(false); }}

点击填空处弹出一个PopupWindow输入框,输入答案后点击确定,
调用fillAnswer方法将答案设置到填空处。

填写答案


前方高能,请减速慢行!
/** * 填写答案 * * @param answer   当前填空处答案 * @param position 填空位置 */private void fillAnswer(String answer, int position) {    answer = " " + answer + " ";
// 替换答案 AnswerRange range = rangeList.get(position); content.replace(range.start, range.end, answer);
// 更新当前的答案范围 AnswerRange currentRange = new AnswerRange(range.start, range.start + answer.length()); rangeList.set(position, currentRange);
// 答案设置下划线 content.setSpan(new UnderlineSpan(), currentRange.start, currentRange.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 将答案添加到集合中 answerList.set(position, answer.replace(" ", ""));
// 更新内容 tvContent.setText(content);
for (int i = 0; i < rangeList.size(); i++) { if (i > position) { // 获取下一个答案原来的范围 AnswerRange oldNextRange = rangeList.get(i); int oldNextAmount = oldNextRange.end - oldNextRange.start; // 计算新旧答案字数的差值 int difference = currentRange.end - range.end;
// 更新下一个答案的范围 AnswerRange nextRange = new AnswerRange(oldNextRange.start + difference, oldNextRange.start + difference + oldNextAmount); rangeList.set(i, nextRange); } }}

首先把填空处的下划线或旧答案替换成新答案,然后更新一下当前的答案范围,由于下划线已经被答案替换了,所以需要为答案设置一条下划线,最后把答案更新到集合中,这样一个填空就完成了。

But,当一个填空处的答案范围改变后,后面所有的填空处答案范围都要跟着改变,所以还需要再更新一下后面填空处的答案范围。首先获取下一个答案原来的范围,计算一下需要向前或向后移动的距离,然后更新一下答案范围就大功告成了。


最后看下如何设置数据
public class MainActivity extends AppCompatActivity {
@BindView(R.id.fbv_content) FillBlankView fbvContent;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this);
initData(); }
private void initData() { String content = "纷纷扬扬的________下了半尺多厚。天地间________的一片。我顺着________工地走了四十多公里," + "只听见各种机器的吼声,可是看不见人影,也看不见工点。一进灵官峡,我就心里发慌。";
// 答案范围集合 List<AnswerRange> rangeList = new ArrayList<>(); rangeList.add(new AnswerRange(5, 13)); rangeList.add(new AnswerRange(23, 31)); rangeList.add(new AnswerRange(38, 46));
fbvContent.setData(content, rangeList); }}

源码地址:
https://github.com/alidili/Demos/tree/master/FillBlankQuestionDemo

到这里就结束啦。
浏览 37
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报