最近需要实现个需求,感觉还挺常用的,并且挺有意思,所以记录一下,要求是显示一段文字,文字中间有填空的地方,用户点击填空的下划线,可以输入内容,输入完成后的内容替换到填空上,这段文字的长度自动变化。
如图:
模拟器效果略卡,接下来说说怎么实现的吧。
1.准备工作:
我们需要先了解SpannableString这个对象类型的使用方法。先来点简单的:
String str = "本人持有____国学生签证,在____国院校就读;"; ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.RED); SpannableString ss = new SpannableString(str); ss.setSpan(colorSpan, 0, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); content.setText(ss);
效果如下:
这里注意一下setSpan的flag有4中取值:
SPAN_INCLUSIVE_EXCLUSIVE表示插入start前的内容和[start,end)左闭右开区间内的内容受到span的影响;
SPAN_INCLUSIVE_INCLUSIVE表示插入start前的内容,插入end后的内容和[start,end)左闭右开区间内的内容受到span的影响;
SPAN_EXCLUSIVE_EXCLUSIVE表示只有[start,end)左闭右开区间内的内容受到span的影响;
SPAN_EXCLUSIVE_INCLUSIVE表示插入end后的内容和[start,end)左闭右开区间内的内容受到span的影响。
值得一说的是网上查到的大部分资料都是像上面这么介绍这四个flag的影响的,
但是这四个flag的影响起作用只在可编辑控件中起作用在不可编辑控件中这四个flag不起任何作用,
例如textview,因为不可编辑控件根本没办法在span生效的内容前或者后插入新的内容,
所以在不可编辑的控件中使用4个flag中的任意一个都可以。
String str = "本人持有____国学生签证,在____国院校就读;"; ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.RED); SpannableString ss = new SpannableString(str); ss.setSpan(colorSpan, 4, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ClickableSpan clickableSpan = new ClickableSpan() { @Override public void onClick(View widget) { Toast.makeText(MainActivity.this, "我被点击了", Toast.LENGTH_SHORT).show(); } }; ss.setSpan(clickableSpan, 4, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //注意这句必须写,否则点击事件不生效 content.setMovementMethod(LinkMovementMethod.getInstance()); content.setText(ss);
效果如下:
好了了解两种就好,剩下的SpannableString可以实现的效果大家可以自己再去学习,用法都大同小异。
2.实现需求效果思路分析:
3.根据思路介绍主要代码:
/**存放Span影响内容的边界信息对象列表*/ private ArrayListranges; /**可变长度的可分组字符串*/ private SpannableStringBuilder ssb; /**存放答案的列表*/ private ArrayList answers; /**用来输入答案的可编辑文本框控件*/ private EditText input; /**用来确定答案的按钮控件*/ private Button sure; /**用来展示输入答案的可编辑文本框和确定按钮界面的popupwindow*/ private PopupWindow popupWindow;
ssb.clear(); ssb.append(str);
for (int i=0; i<ranges.size(); i++) { RangBean bean = ranges.get(i); //设置文字颜色 //这里注意一下setSpan的flag有4中取值(start的取值范围从0开始): //SPAN_INCLUSIVE_EXCLUSIVE表示插入start前的内容和[start,end)左闭右开区间内的内容受到span的影响; //SPAN_INCLUSIVE_INCLUSIVE表示插入start前的内容,插入end后的内容和[start,end)左闭右开区间内的内容受到span的影响; //SPAN_EXCLUSIVE_EXCLUSIVE表示只有[start,end)左闭右开区间内的内容受到span的影响; //SPAN_EXCLUSIVE_INCLUSIVE表示插入end后的内容和[start,end)左闭右开区间内的内容受到span的影响。 //值得一说的是网上查到的大部分资料都是像上面这么介绍这四个flag的影响的, //但是这四个flag的影响起作用只在可编辑控件中起作用 //例如editext中可以,如果想体验可以把我MainActivty注释的edittext // 和activity_main里的相关代码解除注释试一试,在不可编辑控件中这四个flag不起任何 //作用,例如textview,因为不可编辑控件根本没办法在span生效的内容前或者后插入新的内容, // 所以在不可编辑的控件中使用4个flag中的任意一个都可以,你可以试着把下面的flag修改试试效果 ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#4DB6AC")); ssb.setSpan(colorSpan, bean.getStart(), bean.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //设置可点击 ClickableSpan clickableSpan = new MyClickableSpan(i); ssb.setSpan(clickableSpan, bean.getStart(), bean.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //设置下划线 UnderlineSpan underlineSpan = new UnderlineSpan(); ssb.setSpan(underlineSpan, bean.getStart(), bean.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); answers.add(""); } //注意这里必须为文本框设置这个属性,clickablespan才生效 setMovementMethod(LinkMovementMethod.getInstance()); //将可变长度可分组字符串设置到文本框里 setText(ssb)
private class MyClickableSpan extends ClickableSpan { private int index; /** * 构造方法 * @param index 点击位置在边界列表中对应的index */ public MyClickableSpan(int index) { this.index = index; } @Override public void onClick(View widget) { //将答案展示在输入框中 input.setText(answers.get(index)); //设置光标移动到答案最后 input.setSelection(input.length()); //显示popupwindow popupWindow.showAtLocation(EditableTextView.this, Gravity.BOTTOM, 0, 0); //下面两行是弹出软键盘 InputMethodManager innputMethodManager = (InputMethodManager) getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); innputMethodManager.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); sure.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String answer = input.getText().toString(); //将输入框中的答案根据位置添加到答案列表中 answers.set(index, answer); //如果输入框中的答案为空,则用下划线代替 if (TextUtils.isEmpty(answer)) { answers.set(index, ""); answer = "____"; } //获取当前位置的边界信息 RangBean bean = ranges.get(index); //将可变长度可分组字符串中边界信息对应位置替换成答案 ssb.replace(bean.getStart(), bean.getEnd(), answer); //计算边界信息中的答案长度与输入的答案长度的差值 int change = bean.getEnd() - bean.getStart() - answer.length(); //更新当前位置及之后的边界信息 for (int i=index; i<ranges.size(); i++) { RangBean rb = ranges.get(i); //当前位置的起始位置信息不变,之后的所有位置的起始边界信息根据差值移动 if (i != index) { rb.setStart(rb.getStart() - change); } //当前位置以及之后所有位置的结束边界信息根据差值移动 rb.setEnd(rb.getEnd() - change); } //将文本框中的内容更新 setText(ssb); //隐藏popupwindow popupWindow.dismiss(); } }); } }
4.总结和提示:
主要的实现思路和实现代码就是上面这些了,我为了方便使用将这些封装在了一个继承自TextView的自定义EditableTextView中,这样使用的时候只用使用EditableTextView并且将文字和位置信息列表设置好就行,很简单的。提示就是上面这么写完你会发现如果你的软键盘上如果有隐藏按钮或者你直接按物理返回键的时候,软键盘隐藏了,但是Popupwindow却没有隐藏,反正我觉得有点尴尬,所以我又写了个方法监听软键盘的打开和隐藏,如果监听到软键盘隐藏了就让popupwindow也隐藏就好了。废话就不说了,附上demo下载链接:http://download.csdn.net/download/wozuihaole/10266870
5.完整代码:
public class EditableTextView extends android.support.v7.widget.AppCompatTextView { /**存放Span影响内容的边界信息对象列表*/ private ArrayListranges; /**可变长度的可分组字符串*/ private SpannableStringBuilder ssb; /**存放答案的列表*/ private ArrayList answers; /**用来输入答案的可编辑文本框控件*/ private EditText input; /**用来确定答案的按钮控件*/ private Button sure; /**用来展示输入答案的可编辑文本框和确定按钮界面的popupwindow*/ private PopupWindow popupWindow; /**软键盘的高度,我们可以认为屏幕高度的三分之一就是软键盘打开时的高度*/ private int softInputHeight; public EditableTextView(Context context) { super(context); init(); } public EditableTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public EditableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /**初始化*/ private void init() { answers = new ArrayList<>(); ranges = new ArrayList<>(); ssb = new SpannableStringBuilder(); //计算屏幕高度的三分之一,赋值给软键盘高度 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); softInputHeight = dm.heightPixels/3; //加载一个输入框和确定按钮的布局 View view = LayoutInflater.from(getContext()).inflate(R.layout.input, null); input = (EditText) view.findViewById(R.id.editable_textview_input); sure = (Button) view.findViewById(R.id.editable_textview_sure); //初始化popupwindow popupWindow = new PopupWindow(view, ViewGroup.LayoutParams.MATCH_PARENT, dp2px(40)); popupWindow.setFocusable(true); //设置popupwindow会被软键盘顶上去,而不是覆盖掉 popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); //下面的两个方法一起使用,设置点击popupwindow边界外部时使得popupwindow消失 popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000)); popupWindow.setOutsideTouchable(true); initSoftListener(); } /** * 设置显示内容和内容中受span影响的边界信息列表, * 使得文本框中的显示内容的下划线部分的字体颜色改变,并且可以点击, * 点击后弹出输入框,输入内容确定后替换下划线部分的内容 * @param str 显示的内容 * @param rangBeans 内容中受span影响的边界信息列表 * 例如: * str = "本人持有____国学生签证,在____国院校就读;" * RangBean ranges = new ArrayList<>(); * ranges.add(new RangBean(4, 8)); * ranges.add(new RangBean(15, 19)); * setData(str, ranges); */ public void setData(String str, ArrayList rangBeans) { if (TextUtils.isEmpty(str) || rangBeans == null || rangBeans.size() <= 0) { Toast.makeText(getContext(), "参数不能为空", Toast.LENGTH_SHORT).show(); return; } ssb.clear(); ssb.append(str); ranges.clear(); ranges.addAll(rangBeans); for (int i=0; i<ranges.size(); i++) { RangBean bean = ranges.get(i); //设置文字颜色 //这里注意一下setSpan的flag有4中取值(start的取值范围从0开始): //SPAN_INCLUSIVE_EXCLUSIVE表示插入start前的内容和[start,end)左闭右开区间内的内容受到span的影响; //SPAN_INCLUSIVE_INCLUSIVE表示插入start前的内容,插入end后的内容和[start,end)左闭右开区间内的内容受到span的影响; //SPAN_EXCLUSIVE_EXCLUSIVE表示只有[start,end)左闭右开区间内的内容受到span的影响; //SPAN_EXCLUSIVE_INCLUSIVE表示插入end后的内容和[start,end)左闭右开区间内的内容受到span的影响。 //值得一说的是网上查到的大部分资料都是像上面这么介绍这四个flag的影响的, //但是这四个flag的影响起作用只在可编辑控件中起作用 //例如editext中可以,如果想体验可以把我MainActivty注释的edittext // 和activity_main里的相关代码解除注释试一试,在不可编辑控件中这四个flag不起任何 //作用,例如textview,因为不可编辑控件根本没办法在span生效的内容前或者后插入新的内容, // 所以在不可编辑的控件中使用4个flag中的任意一个都可以,你可以试着把下面的flag修改试试效果 ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#4DB6AC")); ssb.setSpan(colorSpan, bean.getStart(), bean.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //设置可点击 ClickableSpan clickableSpan = new MyClickableSpan(i); ssb.setSpan(clickableSpan, bean.getStart(), bean.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //设置下划线 UnderlineSpan underlineSpan = new UnderlineSpan(); ssb.setSpan(underlineSpan, bean.getStart(), bean.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); answers.add(""); } //注意这里必须为文本框设置这个属性,clickablespan才生效 setMovementMethod(LinkMovementMethod.getInstance()); //将可变长度可分组字符串设置到文本框里 setText(ssb); } private class MyClickableSpan extends ClickableSpan { private int index; /** * 构造方法 * @param index 点击位置在边界列表中对应的index */ public MyClickableSpan(int index) { this.index = index; } @Override public void onClick(View widget) { //将答案展示在输入框中 input.setText(answers.get(index)); //设置光标移动到答案最后 input.setSelection(input.length()); //显示popupwindow popupWindow.showAtLocation(EditableTextView.this, Gravity.BOTTOM, 0, 0); //下面两行是弹出软键盘 InputMethodManager innputMethodManager = (InputMethodManager) getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); innputMethodManager.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); sure.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String answer = input.getText().toString(); //将输入框中的答案根据位置添加到答案列表中 answers.set(index, answer); //如果输入框中的答案为空,则用下划线代替 if (TextUtils.isEmpty(answer)) { answers.set(index, ""); answer = "____"; } //获取当前位置的边界信息 RangBean bean = ranges.get(index); //将可变长度可分组字符串中边界信息对应位置替换成答案 ssb.replace(bean.getStart(), bean.getEnd(), answer); //计算边界信息中的答案长度与输入的答案长度的差值 int change = bean.getEnd() - bean.getStart() - answer.length(); //更新当前位置及之后的边界信息 for (int i=index; i<ranges.size(); i++) { RangBean rb = ranges.get(i); //当前位置的起始位置信息不变,之后的所有位置的起始边界信息根据差值移动 if (i != index) { rb.setStart(rb.getStart() - change); } //当前位置以及之后所有位置的结束边界信息根据差值移动 rb.setEnd(rb.getEnd() - change); } //将文本框中的内容更新 setText(ssb); //隐藏popupwindow popupWindow.dismiss(); } }); } } private int dp2px(float dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } /**设置软键盘的关闭和打开监听,要使用这个方法必须手机api在11或以上, * 并且activity在清单配置文件中设置android:windowSoftInputMode="adjustResize"*/ private void initSoftListener(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { ((Activity)getContext()).findViewById(android.R.id.content) .addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { //现在认为只要控件将Activity向上推的高度超过了1/3屏幕高,就认为软键盘弹起 if(oldBottom != 0 && bottom != 0 &&((oldBottom - bottom) > softInputHeight)){ onOpenSoftInput(); }else if(oldBottom != 0 && bottom != 0 &&((bottom - oldBottom) > softInputHeight)){ onCloseSoftInput(); } } }); } } /** * 当软键盘打开时回调 */ private void onOpenSoftInput(){ Log.i("zhangdi","软件盘打开"); } /** * 当软键盘关闭时回调 */ private void onCloseSoftInput(){ Log.i("zhangdi","软件盘关闭"); if (popupWindow != null && popupWindow.isShowing()) { popupWindow.dismiss(); } } }