1.寫在前面
在上一篇文章《Android 使用代碼實現(xiàn)一個填空題》中华糖,我們學(xué)習(xí)了如何實現(xiàn)一個填空題作郭,今天繼續(xù)接著上一篇文章的節(jié)奏诗充,學(xué)習(xí)一下如何實現(xiàn)一個選詞填空題溺健,由于本文中用到了一些上篇文章中的知識點吹艇,還沒有看過上篇文章的同學(xué)可以>戳這里<了解一下惰蜜。
首先看下效果圖:
2.學(xué)習(xí)一些基礎(chǔ)知識
選詞填空題有一個很重要的功能就是拖拽,我們先來學(xué)習(xí)一下如何對View進行拖拽操作受神,寫個簡單的Demo來學(xué)習(xí)下:
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);
// 目標(biāo)區(qū)域設(shè)置拖拽事件監(jiān)聽
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進入目標(biāo)區(qū)域
Log.i("拖拽事件", "被拖拽View進入目標(biāo)區(qū)域");
return true;
case DragEvent.ACTION_DRAG_LOCATION: // 被拖拽View在目標(biāo)區(qū)域移動
Log.i("拖拽事件", "被拖拽View在目標(biāo)區(qū)域移動___X:" + event.getX() + "___Y:" + event.getY());
tvTip.setText("X:" + event.getX() + " Y:" + event.getY());
return true;
case DragEvent.ACTION_DRAG_EXITED: // 被拖拽View離開目標(biāo)區(qū)域
Log.i("拖拽事件", "被拖拽View離開目標(biāo)區(qū)域");
return true;
case DragEvent.ACTION_DROP: // 放開被拖拽View
Log.i("拖拽事件", "放開被拖拽View");
// 釋放拖放陰影蝎抽,并獲取移動數(shù)據(jù)
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;
}
}
看下效果:
看下打印信息:
首先給被拖拽View設(shè)置一個觸摸事件,在onTouch方法中定義一個ClipData對象,傳入文本類型的數(shù)據(jù)“我來了”樟结,當(dāng)觸摸被拖拽View時調(diào)用View的startDrag方法開始移動View养交,此時移動的是被拖拽View的“影子”。
View可以移動了瓢宦,還需要為它設(shè)置一個目標(biāo)區(qū)域碎连,調(diào)用目標(biāo)區(qū)域View的setOnDragListener方法設(shè)置拖拽事件的監(jiān)聽,實現(xiàn)onDrag方法驮履,在ACTION_DRAG_STARTED(拖拽開始)時判斷當(dāng)前接收的是不是文件類型的數(shù)據(jù)鱼辙,如果不是則返回false,不再響應(yīng)拖拽事件玫镐,在ACTION_DROP(放開被拖拽View)時倒戏,釋放拖拽陰影,并獲取傳遞過來的數(shù)據(jù)恐似,通過Toast顯示出來杜跷。
3.實現(xiàn)
首先初始化一些數(shù)據(jù)
public class DragFillBlankView extends RelativeLayout implements View.OnDragListener,
View.OnLongClickListener {
private TextView tvContent;
private LinearLayout llOption;
// 初始數(shù)據(jù)
private String originContent;
// 初始答案范圍集合
private List<AnswerRange> originAnswerRangeList;
// 填空題內(nèi)容
private SpannableStringBuilder content;
// 選項列表
private List<String> optionList;
// 答案范圍集合
private List<AnswerRange> answerRangeList;
// 答案集合
private List<String> 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);
}
...
}
定義一個設(shè)置數(shù)據(jù)的方法,供外部調(diào)用
/**
* 設(shè)置數(shù)據(jù)
*
* @param originContent 源數(shù)據(jù)
* @param optionList 選項列表
* @param answerRangeList 答案范圍集合
*/
public void setData(String originContent, List<String> optionList, List<AnswerRange> answerRangeList) {
if (TextUtils.isEmpty(originContent) || optionList == null || optionList.isEmpty()
|| answerRangeList == null || answerRangeList.isEmpty()) {
return;
}
// 初始數(shù)據(jù)
this.originContent = originContent;
// 初始答案范圍集合
this.originAnswerRangeList = new ArrayList<>();
this.originAnswerRangeList.addAll(answerRangeList);
// 獲取課文內(nèi)容
this.content = new SpannableStringBuilder(originContent);
// 選項列表
this.optionList = optionList;
// 答案范圍集合
this.answerRangeList = answerRangeList;
// 避免重復(fù)創(chuàng)建拖拽選項
if (llOption.getChildCount() < 1) {
// 拖拽選項列表
List<Button> 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 {
// 不顯示已經(jīng)填空的選項
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);
}
}
}
// 設(shè)置下劃線顏色
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("");
}
// 設(shè)置填空處點擊事件
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);
}
// 填空處設(shè)置觸摸事件
tvContent.setMovementMethod(new TouchLinkMovementMethod());
tvContent.setText(content);
tvContent.setOnDragListener(this);
}
首先初始化一些全局?jǐn)?shù)據(jù)矫夷,這個稍后會用到葛闷,然后創(chuàng)建拖拽選項,為了避免重復(fù)創(chuàng)建選項双藕,先判斷選項是否已經(jīng)創(chuàng)建過了淑趾,如果已經(jīng)創(chuàng)建過了,則把已經(jīng)填空的選項隱藏忧陪,然后接下來的邏輯就和普通填空題一樣了扣泊,代碼里已經(jīng)寫了注釋,不再多說嘶摊。
重點看下填空處設(shè)置觸摸事件這里旷赖,由于拖拽是觸摸事件而不是點擊事件,在這里就需要定義一個TouchLinkMovementMethod來響應(yīng)觸摸事件更卒,最后對填空題區(qū)域進行拖拽監(jiān)聽等孵,看下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);
}
}
當(dāng)手指按下的時候,回調(diào)ClickableSpan的onClick方法蹂空,并且不再響應(yīng)點擊事件俯萌。
拖拽開始
@Override
public boolean onLongClick(View v) {
startDrag(v);
return true;
}
/**
* 開始拖拽
*
* @param v 當(dāng)前對象
*/
private void startDrag(View v) {
// 選項內(nèi)容
String optionContent = ((Button) v).getText().toString();
// 記錄當(dāng)前答案選項的位置
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 選項內(nèi)容
* @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;
}
在初始化拖拽選項時,我們?yōu)槊總€Button都設(shè)置了一個長按監(jiān)聽事件上枕,下面來看看它是如何工作的咐熙,首先獲取到當(dāng)前拖拽選項上的答案,作為參數(shù)進行傳遞辨萍,然后記錄選項的位置棋恼,這個是為了當(dāng)拖拽未完成時返弹,重新顯示選項用的,最后在列表中隱藏當(dāng)前拖拽的選項∽ζ現(xiàn)在選項已經(jīng)可以移動了义起,還記得在設(shè)置數(shù)據(jù)的時候我們?yōu)樘羁疹}區(qū)域設(shè)置了拖拽監(jiān)聽,看下它是如何進行響應(yīng)的:
@Override
public 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進入目標(biāo)區(qū)域
return true;
case DragEvent.ACTION_DRAG_LOCATION: // 被拖拽View在目標(biāo)區(qū)域移動
return true;
case DragEvent.ACTION_DRAG_EXITED: // 被拖拽View離開目標(biāo)區(qū)域
return true;
case DragEvent.ACTION_DROP: // 放開被拖拽View
int position = 0;
// 獲取TextView的Layout對象
Layout layout = tvContent.getLayout();
// 當(dāng)前x师崎、y坐標(biāo)
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中字符坐標(biāo)
Rect bound = new Rect();
int line = layout.getLineForOffset(range.start);
layout.getLineBounds(line, bound);
// 字符頂部y坐標(biāo)
int yAxisTop = bound.top - dp2px(10);
// 字符底部y坐標(biāo)
int yAxisBottom = bound.bottom + dp2px(5);
// 字符左邊x坐標(biāo)
float xAxisLeft = layout.getPrimaryHorizontal(range.start) - dp2px(10);
// 字符右邊x坐標(biāo)
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;
}
// 釋放拖放陰影默终,并獲取移動數(shù)據(jù)
ClipData.Item item = event.getClipData().getItemAt(0);
String answer = item.getText().toString();
// 重復(fù)拖拽,在答案列表中顯示原答案
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;
}
如何才能判斷拖拽選項是否到達了某一個填空處呢犁罩?別擔(dān)心齐蔽,在TextView中我們可以獲取到每一個字符的坐標(biāo),當(dāng)放開拖拽選項的時候床估,判斷一下是不是處于某一個填空區(qū)域就大功告成了含滴,別忘了還有填空處跨行的問題需要特殊處理一下。
到達指定位置后丐巫,我們就要把選項中的答案填到題目中了谈况,接下來該輪到fillAnswer方法大顯身手了:
/**
* 填寫答案
*
* @param answer 當(dāng)前填空處答案
* @param position 填空位置
*/
private void fillAnswer(String answer, int position) {
answer = " " + answer + " ";
// 替換答案
AnswerRange range = answerRangeList.get(position);
content.replace(range.start, range.end, answer);
// 更新當(dāng)前的答案范圍
AnswerRange currentRange = new AnswerRange(range.start, range.start + answer.length());
answerRangeList.set(position, currentRange);
// 答案設(shè)置下劃線
content.setSpan(new UnderlineSpan(),
currentRange.start, currentRange.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 將答案添加到集合中
answerList.set(position, answer.replace(" ", ""));
// 更新內(nèi)容
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;
// 計算新舊答案字?jǐn)?shù)的差值
int difference = currentRange.end - range.end;
// 更新下一個答案的范圍
AnswerRange nextRange = new AnswerRange(oldNextRange.start + difference,
oldNextRange.start + difference + oldNextAmount);
answerRangeList.set(i, nextRange);
}
}
}
首先把填空處的下劃線或舊答案替換成新答案,然后更新一下當(dāng)前的答案范圍鞋吉,由于下劃線已經(jīng)被答案替換了鸦做,所以需要為答案設(shè)置一條下劃線励烦,最后把答案更新到集合中谓着,這樣一個填空就完成了。
But坛掠,當(dāng)一個填空處的答案范圍改變后赊锚,后面所有的填空處答案范圍都要跟著改變,所以還需要再更新一下后面填空處的答案范圍屉栓。首先獲取下一個答案原來的范圍舷蒲,計算一下需要向前或向后移動的距離,然后更新一下答案范圍就可以了友多。
在效果圖中我們可以看到牲平,當(dāng)填空完成后,觸摸填空處還可以繼續(xù)拖拽的域滥,繼續(xù)往下看:
/**
* 觸摸事件
*/
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<String> answerList) {
// 重新初始化數(shù)據(jù)
setData(originContent, optionList, originAnswerRangeList);
// 重新填寫已經(jīng)存在的答案
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);
}
}
}
}
當(dāng)觸摸填空處時纵柿,如果此填空處已經(jīng)填寫了答案,則調(diào)用updateAnswer方法把當(dāng)前填空處的答案清除启绰,然后調(diào)用startDrag方法開始進行拖拽昂儒。
最后看下如何設(shè)置數(shù)據(jù)
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 = "紛紛揚揚的________下了半尺多厚。天地間________的一片委可。我順著________工地走了四十多公里渊跋," +
"只聽見各種機器的吼聲,可是看不見人影,也看不見工點拾酝。一進靈官峽燕少,我就心里發(fā)慌。";
// 選項集合
List<String> optionList = new ArrayList<>();
optionList.add("白茫茫");
optionList.add("霧蒙蒙");
optionList.add("鐵路");
optionList.add("公路");
optionList.add("大雪");
// 答案范圍集合
List<AnswerRange> 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);
}
}
4.寫在最后
源碼已托管到GitHub上,歡迎Fork下面,覺得還不錯就Start一下吧徽龟!
歡迎同學(xué)們吐槽評論,如果你覺得本篇博客對你有用镶摘,那么就留個言或者點下喜歡吧(^-^)