26 | * Created by woxingxiao on 2016/01/06. 27 | *
28 | * GitHub: https://github.com/woxingxiao/FillBlankView
29 | */
30 | public class FillBlankView extends AppCompatEditText {
31 |
32 | private static final String INSTANCE_STATE = "saved_instance";
33 | private static final String INSTANCE_PREFIX_STR = "prefix_str";
34 | private static final String INSTANCE_SUFFIX_STR = "suffix_str";
35 |
36 | private int mBlankNum; // the number of blanks
37 | private int mBlankSpace; // the space between two blanks
38 | private int mBlankSolidColor;
39 | private int mBlankStrokeColor;
40 | private int mBlankStrokeWidth;
41 | private int mBlankCornerRadius;
42 | private int mBlankFocusedStrokeColor; // the stroke color of blank when it be focused.
43 | private boolean isPasswordMode; // if true, the contents inputted will be replaced by dots
44 | private int mDotSize;
45 | private int mDotColor;
46 | private int mTextMatchedColor; // if contents matched the original text, the text will show with this color
47 | private int mTextNotMatchedColor; // if contents didn't matched the original text, the text will show with this color
48 | private boolean showTextTemporarily;
49 |
50 | private Paint mPaintBlank;
51 | private Paint mPaintText;
52 | private Paint mPaintDot;
53 | private RectF[] mRectFs;
54 | private RectF mRectBig;
55 | private Rect mTextRect;
56 | private String mPrefixStr;
57 | private String mSuffixStr;
58 | private String[] mBlankStrings;
59 | private int mDotCount;
60 |
61 | private OnTextMatchedListener mListener;
62 | private String originalText;
63 | private Handler mHandler = new Handler(Looper.getMainLooper()) {
64 | @Override
65 | public void handleMessage(Message msg) {
66 | super.handleMessage(msg);
67 |
68 | mHandler.removeCallbacksAndMessages(null);
69 |
70 | showTextTemporarily = false;
71 | invalidate();
72 | }
73 | };
74 |
75 | public FillBlankView(Context context) {
76 | this(context, null);
77 | }
78 |
79 | public FillBlankView(Context context, AttributeSet attrs) {
80 | this(context, attrs, 0);
81 | }
82 |
83 | public FillBlankView(Context context, AttributeSet attrs, int defStyleAttr) {
84 | super(context, attrs, defStyleAttr);
85 |
86 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FillBlankView, defStyleAttr, 0);
87 | mBlankNum = a.getInteger(R.styleable.FillBlankView_blankNum, 6);
88 | mBlankSpace = a.getDimensionPixelSize(R.styleable.FillBlankView_blankSpace, 0);
89 | mBlankSolidColor = a.getColor(R.styleable.FillBlankView_blankSolidColor, getDrawingCacheBackgroundColor());
90 | mBlankStrokeColor = a.getColor(R.styleable.FillBlankView_blankStrokeColor, getCurrentTextColor());
91 | mBlankStrokeWidth = a.getDimensionPixelSize(R.styleable.FillBlankView_blankStrokeWidth, 1);
92 | mBlankCornerRadius = a.getDimensionPixelSize(R.styleable.FillBlankView_blankCornerRadius, 0);
93 | mBlankFocusedStrokeColor = a.getColor(R.styleable.FillBlankView_blankFocusedStrokeColor, mBlankStrokeColor);
94 | isPasswordMode = a.getBoolean(R.styleable.FillBlankView_isPasswordMode, false);
95 | mDotSize = a.getDimensionPixelSize(R.styleable.FillBlankView_dotSize, dp2px(4));
96 | mDotColor = a.getColor(R.styleable.FillBlankView_dotColor, getCurrentTextColor());
97 | mTextMatchedColor = a.getColor(R.styleable.FillBlankView_textMatchedColor, getCurrentTextColor());
98 | mTextNotMatchedColor = a.getColor(R.styleable.FillBlankView_textNotMatchedColor, getCurrentTextColor());
99 | a.recycle();
100 |
101 | int inputType = getInputType();
102 | if (inputType == 129 || inputType == 145 || inputType == 18 || inputType == 225) {
103 | isPasswordMode = true;
104 | }
105 | String text = getText().toString();
106 | if (!text.isEmpty()) {
107 | mBlankNum = text.length();
108 | }
109 | initObjects();
110 |
111 | getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
112 | @Override
113 | public void onGlobalLayout() {
114 | if (Build.VERSION.SDK_INT >= 16) {
115 | getViewTreeObserver().removeOnGlobalLayoutListener(this);
116 | } else {
117 | getViewTreeObserver().removeGlobalOnLayoutListener(this);
118 | }
119 |
120 | setText(getText());
121 | if (isClickable()) {
122 | setFocusable(true);
123 | setFocusableInTouchMode(true);
124 | requestFocus();
125 | }
126 |
127 | mHandler.sendEmptyMessage(0);
128 | }
129 | });
130 | }
131 |
132 | private void initObjects() {
133 | setCursorVisible(false);
134 |
135 | if (mBlankNum <= 0) {
136 | throw new IllegalArgumentException("the 'blankNum' must be greater than zero !");
137 | }
138 | mBlankStrings = new String[mBlankNum];
139 | for (int i = 0; i < mBlankStrings.length; i++) {
140 | mBlankStrings[i] = "";
141 | }
142 |
143 | mPaintBlank = new Paint();
144 | mPaintBlank.setAntiAlias(true);
145 |
146 | mTextRect = new Rect();
147 | mPaintText = new Paint();
148 | mPaintText.setAntiAlias(true);
149 | mPaintText.setColor(getCurrentTextColor());
150 | mPaintText.setTextSize(getTextSize());
151 |
152 | mPaintDot = new Paint();
153 | mPaintDot.setAntiAlias(true);
154 |
155 | addTextChangedListener(new TextWatcher() {
156 | @Override
157 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
158 |
159 | }
160 |
161 | @Override
162 | public void onTextChanged(CharSequence s, int start, int before, int count) {
163 |
164 | }
165 |
166 | @Override
167 | public void afterTextChanged(Editable s) {
168 | if (s.length() > mBlankNum) {
169 | getText().delete(s.length() - 1, s.length());
170 | mDotCount = mBlankNum;
171 | return;
172 | }
173 |
174 | mPaintText.setColor(getCurrentTextColor());
175 | if (isPasswordMode) {
176 | mPaintDot.setColor(mDotColor);
177 | }
178 | for (int i = 0; i < mBlankNum; i++) {
179 | if (i < s.length()) {
180 | mBlankStrings[i] = s.subSequence(i, i + 1).toString();
181 | } else {
182 | mBlankStrings[i] = "";
183 | }
184 | }
185 |
186 | if (getAllText().equals(originalText)) {
187 | if (s.length() == mBlankNum) {
188 | mPaintText.setColor(mTextMatchedColor);
189 | if (isPasswordMode && mTextMatchedColor != getCurrentTextColor()) {
190 | mPaintDot.setColor(mTextMatchedColor);
191 | }
192 | }
193 | if (mListener != null) {
194 | mListener.matched(true, originalText);
195 | }
196 | } else {
197 | if (s.length() == mBlankNum) {
198 | mPaintText.setColor(mTextNotMatchedColor);
199 | if (isPasswordMode && mTextNotMatchedColor != getCurrentTextColor()) {
200 | mPaintDot.setColor(mTextNotMatchedColor);
201 | }
202 | }
203 | if (mListener != null) {
204 | mListener.matched(false, null);
205 | }
206 | }
207 |
208 | int length = s.length();
209 | if (length <= mDotCount) { // deleting
210 | mDotCount = length;
211 | invalidate();
212 |
213 | return;
214 | }
215 |
216 | mDotCount = length;
217 |
218 | if (isPasswordMode) {
219 | showTextTemporarily = true;
220 | invalidate();
221 |
222 | mHandler.sendEmptyMessageDelayed(0, 500);
223 | } else {
224 | invalidate();
225 | }
226 | }
227 | });
228 | }
229 |
230 | @Override
231 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
232 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
233 |
234 | int width = resolveSize(dp2px(80), widthMeasureSpec);
235 | int height = getPaddingTop() + getPaddingBottom() +
236 | (width - getPaddingLeft() - getPaddingRight() - (mBlankNum - 1) * mBlankSpace) / mBlankNum;
237 |
238 | setMeasuredDimension(width, resolveSize(height, heightMeasureSpec));
239 | initSizes();
240 | }
241 |
242 | private void initSizes() {
243 | int viewWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
244 | int column;
245 | if (isEmptyString(mPrefixStr) && isEmptyString(mSuffixStr)) {
246 | column = mBlankNum;
247 | } else if (!isEmptyString(mPrefixStr) && !isEmptyString(mSuffixStr)) {
248 | column = mBlankNum + 2;
249 | } else {
250 | column = mBlankNum + 1;
251 | }
252 | mRectFs = new RectF[column];
253 | int width = (viewWidth - mBlankSpace * (column - 1) - mBlankStrokeWidth) / column;
254 | float strokeHalf = mBlankStrokeWidth / 2f;
255 | float top = getPaddingTop() + strokeHalf;
256 | float bottom = getMeasuredHeight() - getPaddingBottom() - strokeHalf;
257 | float left;
258 | float right;
259 | for (int i = 0; i < mRectFs.length; i++) {
260 | if (i == 0) {
261 | left = getPaddingLeft() + strokeHalf;
262 | } else {
263 | left = width * i + mBlankSpace * i + getPaddingLeft() + strokeHalf;
264 | }
265 | right = left + width;
266 |
267 | mRectFs[i] = new RectF(left, top, right, bottom);
268 | }
269 |
270 | if (mBlankSpace == 0) {
271 | if (mRectBig == null) {
272 | mRectBig = new RectF();
273 | }
274 | if (!isEmptyString(mPrefixStr) && !isEmptyString(mSuffixStr)) {
275 | mRectBig.set(mRectFs[1].left, getPaddingTop(), mRectFs[mRectFs.length - 2].right,
276 | getMeasuredHeight() - getPaddingBottom());
277 | } else if (!isEmptyString(mPrefixStr) && isEmptyString(mSuffixStr)) {
278 | mRectBig.set(mRectFs[1].left, getPaddingTop(), getMeasuredWidth() - getPaddingLeft(),
279 | getMeasuredHeight() - getPaddingBottom());
280 | } else if (isEmptyString(mPrefixStr) && !isEmptyString(mSuffixStr)) {
281 | mRectBig.set(getPaddingLeft(), getPaddingTop(), mRectFs[mRectFs.length - 2].right,
282 | getMeasuredHeight() - getPaddingBottom());
283 | } else {
284 | mRectBig.set(getPaddingLeft(), getPaddingTop(), getMeasuredWidth() - getPaddingLeft(),
285 | getMeasuredHeight() - getPaddingBottom());
286 | }
287 | }
288 | }
289 |
290 | @Override
291 | protected void onDraw(Canvas canvas) {
292 | // super.draw(canvas);
293 |
294 | // draw background
295 | if (getBackground() != null) {
296 | getBackground().draw(canvas);
297 | }
298 |
299 | // draw blanks
300 | for (int i = 0; i < mRectFs.length; i++) {
301 | if (i == 0 && !isEmptyString(mPrefixStr)) {
302 | continue;
303 | }
304 | if (mRectFs.length > 1 && i == mRectFs.length - 1 && !isEmptyString(mSuffixStr)) {
305 | break;
306 | }
307 |
308 | mPaintBlank.setStyle(Paint.Style.FILL);
309 | mPaintBlank.setColor(mBlankSolidColor);
310 | canvas.drawRoundRect(mRectFs[i], mBlankCornerRadius, mBlankCornerRadius, mPaintBlank);
311 |
312 | if (mBlankStrokeWidth > 0) {
313 | mPaintBlank.setStyle(Paint.Style.STROKE);
314 | int index = 0;
315 | boolean allEmpty = false;
316 | for (int j = 0; j < mBlankStrings.length; j++) {
317 | if (mBlankStrings[j].isEmpty()) {
318 | if (j == 0 && mBlankStrings[j].isEmpty()) {
319 | allEmpty = true;
320 | }
321 | index = j;
322 | break;
323 | }
324 | }
325 | if (hasFocus() && i == index) {
326 | mPaintBlank.setColor(mBlankFocusedStrokeColor);
327 | if (index == 0 && !allEmpty) {
328 | mPaintBlank.setColor(mBlankStrokeColor);
329 | }
330 | } else {
331 | mPaintBlank.setColor(mBlankStrokeColor);
332 | }
333 | mPaintBlank.setStrokeWidth(mBlankStrokeWidth);
334 | if (mBlankSpace > 0 && mBlankSolidColor != mBlankStrokeColor) {
335 | canvas.drawRoundRect(mRectFs[i], mBlankCornerRadius, mBlankCornerRadius, mPaintBlank);
336 | } else if (mBlankSpace == 0) {
337 | if (mBlankNum > 1) {
338 | mPaintBlank.setAlpha(110);
339 | mPaintBlank.setStrokeWidth(mBlankStrokeWidth / 2.0f);
340 | canvas.drawLine(mRectFs[i].right, mRectFs[i].top, mRectFs[i].right, mRectFs[i].bottom, mPaintBlank);
341 |
342 | if (i == mRectFs.length - 2) {
343 | mPaintBlank.setAlpha(255);
344 | mPaintBlank.setStrokeWidth(mBlankStrokeWidth);
345 | canvas.drawRoundRect(mRectBig, mBlankCornerRadius, mBlankCornerRadius, mPaintBlank);
346 | break;
347 | }
348 | } else if (mBlankNum == 1) {
349 | canvas.drawRoundRect(mRectBig, mBlankCornerRadius, mBlankCornerRadius, mPaintBlank);
350 | }
351 | }
352 | }
353 | }
354 |
355 | // texts align center of the blank
356 | Paint.FontMetrics fontMetrics = mPaintText.getFontMetrics();
357 | float textCenterY = (getHeight() - fontMetrics.ascent - fontMetrics.descent) / 2.0f;
358 | // draw prefix of original text
359 | if (!isEmptyString(mPrefixStr)) {
360 | mPaintText.setTextAlign(Paint.Align.RIGHT);
361 | mPaintText.getTextBounds(mPrefixStr, 0, mPrefixStr.length(), mTextRect);
362 | canvas.drawText(mPrefixStr, mRectFs[1].left - mBlankSpace, textCenterY, mPaintText);
363 | }
364 | // draw texts or dots on blanks
365 | mPaintText.setTextAlign(Paint.Align.CENTER);
366 | for (int i = 0; i < mBlankNum; i++) {
367 | if (isPasswordMode && mDotCount > 0 && i <= mDotCount - 1) {
368 | if (i + 1 > mDotCount) {
369 | break;
370 | }
371 | if (showTextTemporarily && i == mDotCount - 1) {
372 | mPaintText.getTextBounds(mBlankStrings[i], 0, mBlankStrings[i].length(), mTextRect);
373 | if (isEmptyString(mPrefixStr)) {
374 | canvas.drawText(mBlankStrings[i], mRectFs[i].centerX(), textCenterY, mPaintText);
375 | } else {
376 | canvas.drawText(mBlankStrings[i], mRectFs[i + 1].centerX(), textCenterY, mPaintText);
377 | }
378 | } else {
379 | if (isEmptyString(mPrefixStr)) {
380 | canvas.drawCircle(mRectFs[i].centerX(), mRectFs[i].centerY(), mDotSize, mPaintDot);
381 | } else {
382 | canvas.drawCircle(mRectFs[i + 1].centerX(), mRectFs[i + 1].centerY(), mDotSize, mPaintDot);
383 | }
384 | }
385 | } else {
386 | mPaintText.getTextBounds(mBlankStrings[i], 0, mBlankStrings[i].length(), mTextRect);
387 | if (isEmptyString(mPrefixStr)) {
388 | canvas.drawText(mBlankStrings[i], mRectFs[i].centerX(), textCenterY, mPaintText);
389 | } else {
390 | canvas.drawText(mBlankStrings[i], mRectFs[i + 1].centerX(), textCenterY, mPaintText);
391 | }
392 | }
393 | }
394 | // draw suffix of original text
395 | if (!isEmptyString(mSuffixStr)) {
396 | mPaintText.setTextAlign(Paint.Align.LEFT);
397 | mPaintText.getTextBounds(mSuffixStr, 0, mSuffixStr.length(), mTextRect);
398 | canvas.drawText(mSuffixStr, mRectFs[mRectFs.length - 1].left, textCenterY, mPaintText);
399 | }
400 | }
401 |
402 | public String getOriginalText() {
403 | return originalText;
404 | }
405 |
406 | /**
407 | * set text that waiting to be matched
408 | *
409 | * @param originalText original text
410 | */
411 | public void setOriginalText(@NonNull String originalText) {
412 | this.originalText = originalText;
413 | if (originalText.isEmpty()) {
414 | return;
415 | }
416 |
417 | mBlankNum = originalText.length();
418 | mBlankStrings = new String[mBlankNum];
419 | for (int i = 0; i < mBlankStrings.length; i++) {
420 | mBlankStrings[i] = "";
421 | }
422 | initSizes();
423 | invalidate();
424 | }
425 |
426 | /**
427 | * set text that waiting to be matched
428 | *
429 | * @param originalText original text
430 | * @param prefixLength show length of originalText at start
431 | * @param suffixLength show length of originalText at end
432 | */
433 | public void setOriginalText(@NonNull String originalText, int prefixLength, int suffixLength) {
434 | this.originalText = originalText;
435 | if (originalText.isEmpty()) {
436 | return;
437 | }
438 | if (originalText.length() <= prefixLength + suffixLength) {
439 | throw new IllegalArgumentException("the sum of prefixLength and suffixLength must be less " +
440 | "than length of originalText");
441 | }
442 | mBlankNum = originalText.length() - prefixLength - suffixLength;
443 | mPrefixStr = originalText.substring(0, prefixLength);
444 | mSuffixStr = originalText.substring(originalText.length() - suffixLength, originalText.length());
445 | mBlankStrings = new String[mBlankNum];
446 | for (int i = 0; i < mBlankStrings.length; i++) {
447 | mBlankStrings[i] = "";
448 | }
449 | initSizes();
450 | invalidate();
451 | }
452 |
453 | /**
454 | * Get texts in the blanks
455 | */
456 | public String getFilledText() {
457 | StringBuilder builder = new StringBuilder();
458 | for (String s : mBlankStrings) {
459 | builder.append(s);
460 | }
461 |
462 | return builder.toString();
463 | }
464 |
465 | /**
466 | * prefix + text in the blanks + suffix
467 | */
468 | public String getAllText() {
469 | StringBuilder builder = new StringBuilder();
470 | if (!isEmptyString(mPrefixStr)) {
471 | builder.append(mPrefixStr);
472 | }
473 | for (String s : mBlankStrings) {
474 | builder.append(s);
475 | }
476 | if (!isEmptyString(mSuffixStr)) {
477 | builder.append(mSuffixStr);
478 | }
479 |
480 | return builder.toString();
481 | }
482 |
483 | public int getBlankNum() {
484 | return mBlankNum;
485 | }
486 |
487 | /**
488 | * set number of blanks
489 | */
490 | public void setBlankNum(int blankNum) {
491 | if (!isEmptyString(mPrefixStr) || !isEmptyString(mSuffixStr)) {
492 | return;
493 | }
494 |
495 | mBlankNum = blankNum;
496 | if (mBlankNum <= 0) {
497 | throw new IllegalArgumentException("the 'blankNum' must be greater than zero !");
498 | }
499 | mBlankStrings = new String[mBlankNum];
500 | for (int i = 0; i < mBlankStrings.length; i++) {
501 | mBlankStrings[i] = "";
502 | }
503 | initSizes();
504 | invalidate();
505 | }
506 |
507 | public int getBlankSpace() {
508 | return mBlankSpace;
509 | }
510 |
511 | /**
512 | * set distance between tow blanks
513 | */
514 | public void setBlankSpace(int blankSpace) {
515 | mBlankSpace = blankSpace;
516 | if (mBlankSpace < 0) {
517 | throw new IllegalArgumentException("the number of 'blankSpace' can't be less than zero !");
518 | }
519 | initSizes();
520 | invalidate();
521 | }
522 |
523 | public int getBlankSolidColor() {
524 | return mBlankSolidColor;
525 | }
526 |
527 | public void setBlankSolidColor(int blankSolidColor) {
528 | mBlankSolidColor = blankSolidColor;
529 | invalidate();
530 | }
531 |
532 | public int getBlankStrokeColor() {
533 | return mBlankStrokeColor;
534 | }
535 |
536 | public void setBlankStrokeColor(int blankStrokeColor) {
537 | mBlankStrokeColor = blankStrokeColor;
538 | invalidate();
539 | }
540 |
541 | public int getBlankStrokeWidth() {
542 | return mBlankStrokeWidth;
543 | }
544 |
545 | public void setBlankStrokeWidth(int blankStrokeWidth) {
546 | mBlankStrokeWidth = blankStrokeWidth;
547 | invalidate();
548 | }
549 |
550 | public int getBlankCornerRadius() {
551 | return mBlankCornerRadius;
552 | }
553 |
554 | public void setBlankCornerRadius(int blankCornerRadius) {
555 | mBlankCornerRadius = blankCornerRadius;
556 | invalidate();
557 | }
558 |
559 | public int getDotSize() {
560 | return mDotSize;
561 | }
562 |
563 | public void setDotSize(int dotSize) {
564 | mDotSize = dotSize;
565 | invalidate();
566 | }
567 |
568 | public int getDotColor() {
569 | return mDotColor;
570 | }
571 |
572 | public void setDotColor(int dotColor) {
573 | mDotColor = dotColor;
574 | invalidate();
575 | }
576 |
577 | public int getTextMatchedColor() {
578 | return mTextMatchedColor;
579 | }
580 |
581 | public void setTextMatchedColor(int textMatchedColor) {
582 | mTextMatchedColor = textMatchedColor;
583 | }
584 |
585 | public int getTextNotMatchedColor() {
586 | return mTextNotMatchedColor;
587 | }
588 |
589 | public void setTextNotMatchedColor(int textNotMatchedColor) {
590 | mTextNotMatchedColor = textNotMatchedColor;
591 | }
592 |
593 | public OnTextMatchedListener getOnTextMatchedListener() {
594 | return mListener;
595 | }
596 |
597 | public void setOnTextMatchedListener(OnTextMatchedListener listener) {
598 | mListener = listener;
599 | }
600 |
601 | private int dp2px(int dp) {
602 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
603 | }
604 |
605 | private boolean isEmptyString(String string) {
606 | return string == null || string.isEmpty();
607 | }
608 |
609 | public interface OnTextMatchedListener {
610 | void matched(boolean isMatched, String originalText);
611 | }
612 |
613 | @Override
614 | public Parcelable onSaveInstanceState() {
615 | Bundle bundle = new Bundle();
616 | bundle.putParcelable(INSTANCE_STATE, super.onSaveInstanceState());
617 | bundle.putString(INSTANCE_PREFIX_STR, mPrefixStr);
618 | bundle.putString(INSTANCE_SUFFIX_STR, mSuffixStr);
619 |
620 | return bundle;
621 | }
622 |
623 | @Override
624 | public void onRestoreInstanceState(Parcelable state) {
625 | if (state instanceof Bundle) {
626 | Bundle bundle = (Bundle) state;
627 | mPrefixStr = bundle.getString(INSTANCE_PREFIX_STR);
628 | mSuffixStr = bundle.getString(INSTANCE_SUFFIX_STR);
629 | super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE));
630 |
631 | return;
632 | }
633 | super.onRestoreInstanceState(state);
634 | }
635 |
636 | @Override
637 | protected void onDetachedFromWindow() {
638 | super.onDetachedFromWindow();
639 |
640 | mHandler.removeCallbacksAndMessages(null);
641 | }
642 |
643 | }
644 |
--------------------------------------------------------------------------------
/fillblankview/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |