android studio 点击plain text就闪退_Android使用代码实现一个选词(拖拽)填空题

作者 |  容华谢后 地址 |  https://www.jianshu.com/p/28b7082b7563
在上一篇文章Android使用代码实现一个填空题中,我们学习了如何实现一个填空题,今天继续接着上一篇文章的节奏,学习一下如何实现一个选词填空题,由于本文中用到了一些上篇文章中的知识点,还没有看过上篇文章的同学可以了解一下。 首先看下效果图:

3c80dbd77c8531fd31747aa2a4fc3500.gif

学习一些基础知识

选词填空题有一个很重要的功能就是拖拽,我们先来学习一下如何对View进行拖拽操作,写个简单的Demo来学习下:
public class DragActivity extends BaseActivity implements View.OnDragListener {    @Bind(R.id.tv_tip)    TextView tvTip;    @Bind(R.id.rl_container)    RelativeLayout rlContainer;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_drag);        ButterKnife.bind(this);        // 目标区域设置拖拽事件监听        rlContainer.setOnDragListener(this);    }    @OnTouch(R.id.iv_icon)    public boolean onTouch(View v) {        ClipData.Item item = new ClipData.Item("我来了");        ClipData data = new ClipData(null, new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}, item);        v.startDrag(data, new View.DragShadowBuilder(v), null, 0);        return true;    }    @Override    public boolean onDrag(View v, DragEvent event) {        final int action = event.getAction();        switch (action) {            case DragEvent.ACTION_DRAG_STARTED: // 拖拽开始                Log.i("拖拽事件", "拖拽开始");                return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);            case DragEvent.ACTION_DRAG_ENTERED: // 被拖拽View进入目标区域                Log.i("拖拽事件", "被拖拽View进入目标区域");                return true;            case DragEvent.ACTION_DRAG_LOCATION: // 被拖拽View在目标区域移动                Log.i("拖拽事件", "被拖拽View在目标区域移动___X:" + event.getX() + "___Y:" + event.getY());                tvTip.setText("X:" + event.getX() + "   Y:" + event.getY());                return true;            case DragEvent.ACTION_DRAG_EXITED: // 被拖拽View离开目标区域                Log.i("拖拽事件", "被拖拽View离开目标区域");                return true;            case DragEvent.ACTION_DROP: // 放开被拖拽View                Log.i("拖拽事件", "放开被拖拽View");                // 释放拖放阴影,并获取移动数据                ClipData.Item item = event.getClipData().getItemAt(0);                String content = item.getText().toString();                Toast.makeText(this, content, Toast.LENGTH_SHORT).show();                return true;            case DragEvent.ACTION_DRAG_ENDED: // 拖拽完成                Log.i("拖拽事件", "拖拽完成");                return true;            default:                break;        }        return false;    }}

看下效果:

6e3037f2d19ee194d1945815a998b180.gif

看下打印信息:

0ac466cea8c5d8dbeec5f2acc3f89ef9.png

首先给被拖拽View设置一个触摸事件,在onTouch方法中定义一个ClipData对象,传入文本类型的数据“我来了”,当触摸被拖拽View时调用View的startDrag方法开始移动View,此时移动的是被拖拽View的“影子”。 View可以移动了,还需要为它设置一个目标区域,调用目标区域View的setOnDragListener方法设置拖拽事件的监听,实现onDrag方法,在ACTION_DRAG_STARTED(拖拽开始)时判断当前接收的是不是文件类型的数据,如果不是则返回false,不再响应拖拽事件,在ACTION_DROP(放开被拖拽View)时,释放拖拽阴影,并获取传递过来的数据,通过Toast显示出来。

实现

首先初始化一些数据

public class DragFillBlankView extends RelativeLayout implements View.OnDragListener,        View.OnLongClickListener {    private TextView tvContent;    private LinearLayout llOption;    // 初始数据    private String originContent;    // 初始答案范围集合    private List originAnswerRangeList;    // 填空题内容    private SpannableStringBuilder content;    // 选项列表    private List optionList;    // 答案范围集合    private List answerRangeList;    // 答案集合    private List answerList;    // 选项位置    private int optionPosition;    // 一次拖拽填空是否完成    private boolean isFillBlank;    public DragFillBlankView(Context context) {        this(context, null);    }    public DragFillBlankView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public DragFillBlankView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    private void initView() {        LayoutInflater inflater = LayoutInflater.from(getContext());        inflater.inflate(R.layout.layout_drag_fill_blank, this);        tvContent = (TextView) findViewById(R.id.tv_content);        llOption = (LinearLayout) findViewById(R.id.ll_option);    }    ...}

定义一个设置数据的方法,供外部调用

/** * 设置数据 * * @param originContent   源数据 * @param optionList      选项列表 * @param answerRangeList 答案范围集合 */public void setData(String originContent, List optionList, List answerRangeList) {    if (TextUtils.isEmpty(originContent) || optionList == null || optionList.isEmpty()            || answerRangeList == null || answerRangeList.isEmpty()) {        return;    }    // 初始数据    this.originContent = originContent;    // 初始答案范围集合    this.originAnswerRangeList = new ArrayList<>();    this.originAnswerRangeList.addAll(answerRangeList);    // 获取课文内容    this.content = new SpannableStringBuilder(originContent);    // 选项列表    this.optionList = optionList;    // 答案范围集合    this.answerRangeList = answerRangeList;    // 避免重复创建拖拽选项    if (llOption.getChildCount() < 1) {        // 拖拽选项列表        List itemList = new ArrayList<>();        for (String option : optionList) {            Button btnAnswer = new Button(getContext());            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);            params.setMargins(0, 0, dp2px(10), 0);            btnAnswer.setLayoutParams(params);            btnAnswer.setBackgroundColor(Color.parseColor("#4DB6AC"));            btnAnswer.setTextColor(Color.WHITE);            btnAnswer.setText(option);            btnAnswer.setOnLongClickListener(this);            itemList.add(btnAnswer);        }        // 显示拖拽选项        for (int i = 0; i < itemList.size(); i++) {            llOption.addView(itemList.get(i));        }    } else {        // 不显示已经填空的选项        for (int i = 0; i < llOption.getChildCount(); i++) {            Button button = (Button) llOption.getChildAt(i);            String option = button.getText().toString();            if (!answerList.isEmpty() && answerList.contains(option)) {                button.setVisibility(INVISIBLE);            } else {                button.setVisibility(VISIBLE);            }        }    }    // 设置下划线颜色    for (AnswerRange range : this.answerRangeList) {        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 < answerRangeList.size(); i++) {        answerList.add("");    }    // 设置填空处点击事件    for (int i = 0; i < this.answerRangeList.size(); i++) {        AnswerRange range = this.answerRangeList.get(i);        BlankClickableSpan blankClickableSpan = new BlankClickableSpan(i);        content.setSpan(blankClickableSpan, range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);    }    // 填空处设置触摸事件    tvContent.setMovementMethod(new TouchLinkMovementMethod());    tvContent.setText(content);    tvContent.setOnDragListener(this);}
首先初始化一些全局数据,这个稍后会用到,然后创建拖拽选项,为了避免重复创建选项,先判断选项是否已经创建过了,如果已经创建过了,则把已经填空的选项隐藏,然后接下来的逻辑就和普通填空题一样了,代码里已经写了注释,不再多说。 重点看下填空处设置触摸事件这里,由于拖拽是触摸事件而不是点击事件,在这里就需要定义一个TouchLinkMovementMethod来响应触摸事件,最后对填空题区域进行拖拽监听,看下TouchLinkMovementMethod类:
public class TouchLinkMovementMethod extends LinkMovementMethod {    @Override    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {        int action = event.getAction();        if (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);            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);            if (link.length != 0) {                link[0].onClick(widget);                return true;            } else {                Selection.removeSelection(buffer);            }        }        return super.onTouchEvent(widget, buffer, event);    }}
当手指按下的时候,回调ClickableSpan的onClick方法,并且不再响应点击事件。

拖拽开始

@Overridepublic boolean onLongClick(View v) {    startDrag(v);    return true;}/** * 开始拖拽 * * @param v 当前对象 */private void startDrag(View v) {    // 选项内容    String optionContent = ((Button) v).getText().toString();    // 记录当前答案选项的位置    optionPosition = getOptionPosition(optionContent);    // 开始拖拽后在列表中隐藏答案选项    v.setVisibility(INVISIBLE);    ClipData.Item item = new ClipData.Item(optionContent);    ClipData data = new ClipData(null, new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}, item);    v.startDrag(data, new DragShadowBuilder(v), null, 0);}/** * 获取选项位置 * * @param option 选项内容 * @return 选项位置 */private int getOptionPosition(String option) {    for (int i = 0; i < llOption.getChildCount(); i++) {        Button btnOption = (Button) llOption.getChildAt(i);        if (btnOption.getText().toString().equals(option)) {            return i;        }    }    return 0;}
在初始化拖拽选项时,我们为每个Button都设置了一个长按监听事件,下面来看看它是如何工作的,首先获取到当前拖拽选项上的答案,作为参数进行传递,然后记录选项的位置,这个是为了当拖拽未完成时,重新显示选项用的,最后在列表中隐藏当前拖拽的选项。现在选项已经可以移动了,还记得在设置数据的时候我们为填空题区域设置了拖拽监听,看下它是如何进行响应的:
@Overridepublic boolean onDrag(View v, DragEvent event) {    final int action = event.getAction();    switch (action) {        case DragEvent.ACTION_DRAG_STARTED: // 拖拽开始            return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);        case DragEvent.ACTION_DRAG_ENTERED: // 被拖拽View进入目标区域            return true;        case DragEvent.ACTION_DRAG_LOCATION: // 被拖拽View在目标区域移动            return true;        case DragEvent.ACTION_DRAG_EXITED: // 被拖拽View离开目标区域            return true;        case DragEvent.ACTION_DROP: // 放开被拖拽View            int position = 0;            // 获取TextView的Layout对象            Layout layout = tvContent.getLayout();            // 当前x、y坐标            float currentX = event.getX();            float currentY = event.getY();            // 如果拖拽答案没有进行填空则return            boolean isContinue = false;            for (int i = 0; i < answerRangeList.size(); i++) {                AnswerRange range = answerRangeList.get(i);                // 获取TextView中字符坐标                Rect bound = new Rect();                int line = layout.getLineForOffset(range.start);                layout.getLineBounds(line, bound);                // 字符顶部y坐标                int yAxisTop = bound.top - dp2px(10);                // 字符底部y坐标                int yAxisBottom = bound.bottom + dp2px(5);                // 字符左边x坐标                float xAxisLeft = layout.getPrimaryHorizontal(range.start) - dp2px(10);                // 字符右边x坐标                float xAxisRight = layout.getSecondaryHorizontal(range.end) + dp2px(10);                if (xAxisRight > xAxisLeft) { // 填空在一行                    if (currentX > xAxisLeft && currentX < xAxisRight &&                            currentY < yAxisBottom && currentY > yAxisTop) {                        position = i;                        isContinue = true;                        break;                    }                } else { // 跨行填空                    if ((currentX > xAxisLeft || currentX < xAxisRight) &&                            currentY < yAxisBottom && currentY > yAxisTop) {                        position = i;                        isContinue = true;                        break;                    }                }            }            if (!isContinue) {                return true;            }            // 释放拖放阴影,并获取移动数据            ClipData.Item item = event.getClipData().getItemAt(0);            String answer = item.getText().toString();            // 重复拖拽,在答案列表中显示原答案            String oldAnswer = answerList.get(position);            if (!TextUtils.isEmpty(oldAnswer)) {                llOption.getChildAt(getOptionPosition(oldAnswer)).setVisibility(VISIBLE);            }            // 填写答案            fillAnswer(answer, position);            isFillBlank = true;            return true;        case DragEvent.ACTION_DRAG_ENDED: // 拖拽完成            if (!isFillBlank) {                llOption.getChildAt(optionPosition).setVisibility(VISIBLE);            } else {                isFillBlank = false;            }            return true;        default:            break;    }    return false;}
如何才能判断拖拽选项是否到达了某一个填空处呢?别担心,在TextView中我们可以获取到每一个字符的坐标,当放开拖拽选项的时候,判断一下是不是处于某一个填空区域就大功告成了,别忘了还有填空处跨行的问题需要特殊处理一下。 到达指定位置后,我们就要把选项中的答案填到题目中了,接下来该轮到fillAnswer方法大显身手了:
/** * 填写答案 * * @param answer   当前填空处答案 * @param position 填空位置 */private void fillAnswer(String answer, int position) {    answer = " " + answer + " ";    // 替换答案    AnswerRange range = answerRangeList.get(position);    content.replace(range.start, range.end, answer);    // 更新当前的答案范围    AnswerRange currentRange = new AnswerRange(range.start, range.start + answer.length());    answerRangeList.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 < answerRangeList.size(); i++) {        if (i > position) {            // 获取下一个答案原来的范围            AnswerRange oldNextRange = answerRangeList.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);            answerRangeList.set(i, nextRange);        }    }}
首先把填空处的下划线或旧答案替换成新答案,然后更新一下当前的答案范围,由于下划线已经被答案替换了,所以需要为答案设置一条下划线,最后把答案更新到集合中,这样一个填空就完成了。 But,当一个填空处的答案范围改变后,后面所有的填空处答案范围都要跟着改变,所以还需要再更新一下后面填空处的答案范围。首先获取下一个答案原来的范围,计算一下需要向前或向后移动的距离,然后更新一下答案范围就可以了。 在效果图中我们可以看到,当填空完成后,触摸填空处还可以继续拖拽的,继续往下看:

3c80dbd77c8531fd31747aa2a4fc3500.gif

/** * 触摸事件 */class BlankClickableSpan extends ClickableSpan {    private int position;    public BlankClickableSpan(int position) {        this.position = position;    }    @Override    public void onClick(final View widget) {        // 显示原有答案        String oldAnswer = answerList.get(position);        if (!TextUtils.isEmpty(oldAnswer)) {            answerList.set(position, "");            updateAnswer(answerList);            startDrag(llOption.getChildAt(getOptionPosition(oldAnswer)));        }    }    @Override    public void updateDrawState(TextPaint ds) {        // 不显示下划线        ds.setUnderlineText(false);    }}/** * 更新答案 * * @param answerList 答案列表 */public void updateAnswer(List answerList) {    // 重新初始化数据    setData(originContent, optionList, originAnswerRangeList);    // 重新填写已经存在的答案    if (answerList != null && !answerList.isEmpty()) {        for (int i = 0; i < answerList.size(); i++) {            String answer = answerList.get(i);            if (!TextUtils.isEmpty(answer)) {                fillAnswer(answer, i);            }        }    }}
当触摸填空处时,如果此填空处已经填写了答案,则调用updateAnswer方法把当前填空处的答案清除,然后调用startDrag方法开始进行拖拽。

最后看下如何设置数据

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

源码地址:

https://github.com/alidili/Demos/tree/master/DragFillBlankQuestionDemo

到这里就结束啦 往期精彩回顾:
  • Android实现短信验证码自动填充功能

  • Android仿echo精美弹幕功能

  • Android实现头像重叠排列功能

  • Android仿QQ个性标签功能

  • Android仿QQ侧滑删除的功能

e9122a5de6d81318a147b66e76403379.png

59f1f4b842a760c5730bcd1732e46054.png

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值