作者 | 容华谢后 地址 | https://www.jianshu.com/p/3c8a1c987cea刚看到要做填空题这个需求的时候,第一个反应是到百度,啊...不对,谷歌上搜一下有没有类似的Demo,无奈搜出来的全是Android面试题,唉,算了,还是老老实实自己实现吧,先看下效果
2.学习一些基础知识
首先来学习一下如何对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(); } }}
看下效果:
Spanned.SPAN_INCLUSIVE_INCLUSIVE:前后都包括
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE:前后都不包括
Spanned.SPAN_INCLUSIVE_EXCLUSIVE:前面包括,后面不包括
Spanned.SPAN_EXCLUSIVE_INCLUSIVE:前面不包括,后面包括
实现
首先初始化一些数据
public class FillBlankView extends RelativeLayout { private TextView tvContent; private Context context; // 答案集合 private List answerList; // 答案范围集合 private List 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 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 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
到这里就结束啦
往期精彩回顾:
Android实现短信验证码自动填充功能
Android仿echo精美弹幕功能
Android实现头像重叠排列功能
Android仿QQ个性标签功能
Android仿QQ侧滑删除的功能