1.写在前面
最近项目比较忙,有一个多月没有更新博客了,利用闲暇之余总结一下项目中遇到的问题,分享给大家!
刚看到要做填空题这个需求的时候,第一个反应是到百度,啊...不对,谷歌上搜一下有没有类似的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();
}
}
}
看下效果:
简单说下,首先把要显示的内容转成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(前面包括,后面不包括)。
是不是清晰了很多,如果图还看不懂,慢走不送!
3.实现
首先初始化一些数据
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);
}
}
4.写在最后
源码已托管到GitHub上,欢迎Fork,觉得还不错就Start一下吧!
欢迎同学们吐槽评论,如果你觉得本篇博客对你有用,那么就留个言或者点下喜欢吧(^-^)
明天就是国庆节了,祝大家国庆快乐哈!
在下一篇文章中,我们将会学习一下如何实现一个拖拽填空题(选词填空),敬请期待!