From e2ca6248394207b5e0f2b9babef885bbd6071559 Mon Sep 17 00:00:00 2001
From: Hans <99849318@qq.com>
Date: Mon, 10 Oct 2016 21:52:44 +0800
Subject: [PATCH] Change the current state to APPEND MODEL or not
---
.../me/gujun/android/taggroup/TagGroup.java | 2158 +++++++++--------
1 file changed, 1134 insertions(+), 1024 deletions(-)
diff --git a/library/src/main/java/me/gujun/android/taggroup/TagGroup.java b/library/src/main/java/me/gujun/android/taggroup/TagGroup.java
index 5f2ae99..f72f29d 100644
--- a/library/src/main/java/me/gujun/android/taggroup/TagGroup.java
+++ b/library/src/main/java/me/gujun/android/taggroup/TagGroup.java
@@ -1,1025 +1,1135 @@
-package me.gujun.android.taggroup;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.DashPathEffect;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PathEffect;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.text.method.ArrowKeyMovementMethod;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputConnectionWrapper;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A TagGroup
is a special layout with a set of tags.
- * This group has two modes:
- *
- * 1. APPEND mode
- * 2. DISPLAY mode
- *
- * Default is DISPLAY mode. When in APPEND mode, the group is capable of input for append new tags
- * and delete tags.
- *
- * When in DISPLAY mode, the group is only contain NORMAL state tags, and the tags in group
- * is not focusable.
- *
- *
- * @author Jun Gu (http://2dxgujun.com)
- * @version 2.0
- * @since 2015-2-3 14:16:32
- */
-public class TagGroup extends ViewGroup {
- private final int default_border_color = Color.rgb(0x49, 0xC1, 0x20);
- private final int default_text_color = Color.rgb(0x49, 0xC1, 0x20);
- private final int default_background_color = Color.WHITE;
- private final int default_dash_border_color = Color.rgb(0xAA, 0xAA, 0xAA);
- private final int default_input_hint_color = Color.argb(0x80, 0x00, 0x00, 0x00);
- private final int default_input_text_color = Color.argb(0xDE, 0x00, 0x00, 0x00);
- private final int default_checked_border_color = Color.rgb(0x49, 0xC1, 0x20);
- private final int default_checked_text_color = Color.WHITE;
- private final int default_checked_marker_color = Color.WHITE;
- private final int default_checked_background_color = Color.rgb(0x49, 0xC1, 0x20);
- private final int default_pressed_background_color = Color.rgb(0xED, 0xED, 0xED);
- private final float default_border_stroke_width;
- private final float default_text_size;
- private final float default_horizontal_spacing;
- private final float default_vertical_spacing;
- private final float default_horizontal_padding;
- private final float default_vertical_padding;
-
- /** Indicates whether this TagGroup is set up to APPEND mode or DISPLAY mode. Default is false. */
- private boolean isAppendMode;
-
- /** The text to be displayed when the text of the INPUT tag is empty. */
- private CharSequence inputHint;
-
- /** The tag outline border color. */
- private int borderColor;
-
- /** The tag text color. */
- private int textColor;
-
- /** The tag background color. */
- private int backgroundColor;
-
- /** The dash outline border color. */
- private int dashBorderColor;
-
- /** The input tag hint text color. */
- private int inputHintColor;
-
- /** The input tag type text color. */
- private int inputTextColor;
-
- /** The checked tag outline border color. */
- private int checkedBorderColor;
-
- /** The check text color */
- private int checkedTextColor;
-
- /** The checked marker color. */
- private int checkedMarkerColor;
-
- /** The checked tag background color. */
- private int checkedBackgroundColor;
-
- /** The tag background color, when the tag is being pressed. */
- private int pressedBackgroundColor;
-
- /** The tag outline border stroke width, default is 0.5dp. */
- private float borderStrokeWidth;
-
- /** The tag text size, default is 13sp. */
- private float textSize;
-
- /** The horizontal tag spacing, default is 8.0dp. */
- private int horizontalSpacing;
-
- /** The vertical tag spacing, default is 4.0dp. */
- private int verticalSpacing;
-
- /** The horizontal tag padding, default is 12.0dp. */
- private int horizontalPadding;
-
- /** The vertical tag padding, default is 3.0dp. */
- private int verticalPadding;
-
- /** Listener used to dispatch tag change event. */
- private OnTagChangeListener mOnTagChangeListener;
-
- /** Listener used to dispatch tag click event. */
- private OnTagClickListener mOnTagClickListener;
-
- /** Listener used to handle tag click event. */
- private InternalTagClickListener mInternalTagClickListener = new InternalTagClickListener();
-
- public TagGroup(Context context) {
- this(context, null);
- }
-
- public TagGroup(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.tagGroupStyle);
- }
-
- public TagGroup(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- default_border_stroke_width = dp2px(0.5f);
- default_text_size = sp2px(13.0f);
- default_horizontal_spacing = dp2px(8.0f);
- default_vertical_spacing = dp2px(4.0f);
- default_horizontal_padding = dp2px(12.0f);
- default_vertical_padding = dp2px(3.0f);
-
- // Load styled attributes.
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TagGroup, defStyleAttr, R.style.TagGroup);
- try {
- isAppendMode = a.getBoolean(R.styleable.TagGroup_atg_isAppendMode, false);
- inputHint = a.getText(R.styleable.TagGroup_atg_inputHint);
- borderColor = a.getColor(R.styleable.TagGroup_atg_borderColor, default_border_color);
- textColor = a.getColor(R.styleable.TagGroup_atg_textColor, default_text_color);
- backgroundColor = a.getColor(R.styleable.TagGroup_atg_backgroundColor, default_background_color);
- dashBorderColor = a.getColor(R.styleable.TagGroup_atg_dashBorderColor, default_dash_border_color);
- inputHintColor = a.getColor(R.styleable.TagGroup_atg_inputHintColor, default_input_hint_color);
- inputTextColor = a.getColor(R.styleable.TagGroup_atg_inputTextColor, default_input_text_color);
- checkedBorderColor = a.getColor(R.styleable.TagGroup_atg_checkedBorderColor, default_checked_border_color);
- checkedTextColor = a.getColor(R.styleable.TagGroup_atg_checkedTextColor, default_checked_text_color);
- checkedMarkerColor = a.getColor(R.styleable.TagGroup_atg_checkedMarkerColor, default_checked_marker_color);
- checkedBackgroundColor = a.getColor(R.styleable.TagGroup_atg_checkedBackgroundColor, default_checked_background_color);
- pressedBackgroundColor = a.getColor(R.styleable.TagGroup_atg_pressedBackgroundColor, default_pressed_background_color);
- borderStrokeWidth = a.getDimension(R.styleable.TagGroup_atg_borderStrokeWidth, default_border_stroke_width);
- textSize = a.getDimension(R.styleable.TagGroup_atg_textSize, default_text_size);
- horizontalSpacing = (int) a.getDimension(R.styleable.TagGroup_atg_horizontalSpacing, default_horizontal_spacing);
- verticalSpacing = (int) a.getDimension(R.styleable.TagGroup_atg_verticalSpacing, default_vertical_spacing);
- horizontalPadding = (int) a.getDimension(R.styleable.TagGroup_atg_horizontalPadding, default_horizontal_padding);
- verticalPadding = (int) a.getDimension(R.styleable.TagGroup_atg_verticalPadding, default_vertical_padding);
- } finally {
- a.recycle();
- }
-
- if (isAppendMode) {
- // Append the initial INPUT tag.
- appendInputTag();
-
- // Set the click listener to detect the end-input event.
- setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- submitTag();
- }
- });
- }
- }
-
- /**
- * Call this to submit the INPUT tag.
- */
- public void submitTag() {
- final TagView inputTag = getInputTag();
- if (inputTag != null && inputTag.isInputAvailable()) {
- inputTag.endInput();
-
- if (mOnTagChangeListener != null) {
- mOnTagChangeListener.onAppend(TagGroup.this, inputTag.getText().toString());
- }
- appendInputTag();
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- measureChildren(widthMeasureSpec, heightMeasureSpec);
-
- int width = 0;
- int height = 0;
-
- int row = 0; // The row counter.
- int rowWidth = 0; // Calc the current row width.
- int rowMaxHeight = 0; // Calc the max tag height, in current row.
-
- final int count = getChildCount();
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- final int childWidth = child.getMeasuredWidth();
- final int childHeight = child.getMeasuredHeight();
-
- if (child.getVisibility() != GONE) {
- rowWidth += childWidth;
- if (rowWidth > widthSize) { // Next line.
- rowWidth = childWidth; // The next row width.
- height += rowMaxHeight + verticalSpacing;
- rowMaxHeight = childHeight; // The next row max height.
- row++;
- } else { // This line.
- rowMaxHeight = Math.max(rowMaxHeight, childHeight);
- }
- rowWidth += horizontalSpacing;
- }
- }
- // Account for the last row height.
- height += rowMaxHeight;
-
- // Account for the padding too.
- height += getPaddingTop() + getPaddingBottom();
-
- // If the tags grouped in one row, set the width to wrap the tags.
- if (row == 0) {
- width = rowWidth;
- width += getPaddingLeft() + getPaddingRight();
- } else {// If the tags grouped exceed one line, set the width to match the parent.
- width = widthSize;
- }
-
- setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
- heightMode == MeasureSpec.EXACTLY ? heightSize : height);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- final int parentLeft = getPaddingLeft();
- final int parentRight = r - l - getPaddingRight();
- final int parentTop = getPaddingTop();
- final int parentBottom = b - t - getPaddingBottom();
-
- int childLeft = parentLeft;
- int childTop = parentTop;
-
- int rowMaxHeight = 0;
-
- final int count = getChildCount();
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- final int width = child.getMeasuredWidth();
- final int height = child.getMeasuredHeight();
-
- if (child.getVisibility() != GONE) {
- if (childLeft + width > parentRight) { // Next line
- childLeft = parentLeft;
- childTop += rowMaxHeight + verticalSpacing;
- rowMaxHeight = height;
- } else {
- rowMaxHeight = Math.max(rowMaxHeight, height);
- }
- child.layout(childLeft, childTop, childLeft + width, childTop + height);
-
- childLeft += width + horizontalSpacing;
- }
- }
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState ss = new SavedState(superState);
- ss.tags = getTags();
- ss.checkedPosition = getCheckedTagIndex();
- if (getInputTag() != null) {
- ss.input = getInputTag().getText().toString();
- }
- return ss;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- if (!(state instanceof SavedState)) {
- super.onRestoreInstanceState(state);
- return;
- }
-
- SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(ss.getSuperState());
-
- setTags(ss.tags);
- TagView checkedTagView = getTagAt(ss.checkedPosition);
- if (checkedTagView != null) {
- checkedTagView.setChecked(true);
- }
- if (getInputTag() != null) {
- getInputTag().setText(ss.input);
- }
- }
-
- /**
- * Returns the INPUT tag view in this group.
- *
- * @return the INPUT state tag view or null if not exists
- */
- protected TagView getInputTag() {
- if (isAppendMode) {
- final int inputTagIndex = getChildCount() - 1;
- final TagView inputTag = getTagAt(inputTagIndex);
- if (inputTag != null && inputTag.mState == TagView.STATE_INPUT) {
- return inputTag;
- } else {
- return null;
- }
- } else {
- return null;
- }
- }
-
- /**
- * Returns the INPUT state tag in this group.
- *
- * @return the INPUT state tag view or null if not exists
- */
- public String getInputTagText() {
- final TagView inputTagView = getInputTag();
- if (inputTagView != null) {
- return inputTagView.getText().toString();
- }
- return null;
- }
-
- /**
- * Return the last NORMAL state tag view in this group.
- *
- * @return the last NORMAL state tag view or null if not exists
- */
- protected TagView getLastNormalTagView() {
- final int lastNormalTagIndex = isAppendMode ? getChildCount() - 2 : getChildCount() - 1;
- TagView lastNormalTagView = getTagAt(lastNormalTagIndex);
- return lastNormalTagView;
- }
-
- /**
- * Returns the tag array in group, except the INPUT tag.
- *
- * @return the tag array.
- */
- public String[] getTags() {
- final int count = getChildCount();
- final List tagList = new ArrayList<>();
- for (int i = 0; i < count; i++) {
- final TagView tagView = getTagAt(i);
- if (tagView.mState == TagView.STATE_NORMAL) {
- tagList.add(tagView.getText().toString());
- }
- }
-
- return tagList.toArray(new String[tagList.size()]);
- }
-
- /**
- * @see #setTags(String...)
- */
- public void setTags(List tagList) {
- setTags(tagList.toArray(new String[tagList.size()]));
- }
-
- /**
- * Set the tags. It will remove all previous tags first.
- *
- * @param tags the tag list to set.
- */
- public void setTags(String... tags) {
- removeAllViews();
- for (final String tag : tags) {
- appendTag(tag);
- }
-
- if (isAppendMode) {
- appendInputTag();
- }
- }
-
- /**
- * Returns the tag view at the specified position in the group.
- *
- * @param index the position at which to get the tag view from.
- * @return the tag view at the specified position or null if the position
- * does not exists within this group.
- */
- protected TagView getTagAt(int index) {
- return (TagView) getChildAt(index);
- }
-
- /**
- * Returns the checked tag view in the group.
- *
- * @return the checked tag view or null if not exists.
- */
- protected TagView getCheckedTag() {
- final int checkedTagIndex = getCheckedTagIndex();
- if (checkedTagIndex != -1) {
- return getTagAt(checkedTagIndex);
- }
- return null;
- }
-
- /**
- * Return the checked tag index.
- *
- * @return the checked tag index, or -1 if not exists.
- */
- protected int getCheckedTagIndex() {
- final int count = getChildCount();
- for (int i = 0; i < count; i++) {
- final TagView tag = getTagAt(i);
- if (tag.isChecked) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * Register a callback to be invoked when this tag group is changed.
- *
- * @param l the callback that will run
- */
- public void setOnTagChangeListener(OnTagChangeListener l) {
- mOnTagChangeListener = l;
- }
-
- /**
- * @see #appendInputTag(String)
- */
- protected void appendInputTag() {
- appendInputTag(null);
- }
-
- /**
- * Append a INPUT tag to this group. It will throw an exception if there has a previous INPUT tag.
- *
- * @param tag the tag text.
- */
- protected void appendInputTag(String tag) {
- final TagView previousInputTag = getInputTag();
- if (previousInputTag != null) {
- throw new IllegalStateException("Already has a INPUT tag in group.");
- }
-
- final TagView newInputTag = new TagView(getContext(), TagView.STATE_INPUT, tag);
- newInputTag.setOnClickListener(mInternalTagClickListener);
- addView(newInputTag);
- }
-
- /**
- * Append tag to this group.
- *
- * @param tag the tag to append.
- */
- protected void appendTag(CharSequence tag) {
- final TagView newTag = new TagView(getContext(), TagView.STATE_NORMAL, tag);
- newTag.setOnClickListener(mInternalTagClickListener);
- addView(newTag);
- }
-
- public float dp2px(float dp) {
- return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
- getResources().getDisplayMetrics());
- }
-
- public float sp2px(float sp) {
- return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
- getResources().getDisplayMetrics());
- }
-
- @Override
- public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new TagGroup.LayoutParams(getContext(), attrs);
- }
-
- /**
- * Register a callback to be invoked when a tag is clicked.
- *
- * @param l the callback that will run.
- */
- public void setOnTagClickListener(OnTagClickListener l) {
- mOnTagClickListener = l;
- }
-
- protected void deleteTag(TagView tagView) {
- removeView(tagView);
- if (mOnTagChangeListener != null) {
- mOnTagChangeListener.onDelete(TagGroup.this, tagView.getText().toString());
- }
- }
-
- /**
- * Interface definition for a callback to be invoked when a tag group is changed.
- */
- public interface OnTagChangeListener {
- /**
- * Called when a tag has been appended to the group.
- *
- * @param tag the appended tag.
- */
- void onAppend(TagGroup tagGroup, String tag);
-
- /**
- * Called when a tag has been deleted from the the group.
- *
- * @param tag the deleted tag.
- */
- void onDelete(TagGroup tagGroup, String tag);
- }
-
- /**
- * Interface definition for a callback to be invoked when a tag is clicked.
- */
- public interface OnTagClickListener {
- /**
- * Called when a tag has been clicked.
- *
- * @param tag The tag text of the tag that was clicked.
- */
- void onTagClick(String tag);
- }
-
- /**
- * Per-child layout information for layouts.
- */
- public static class LayoutParams extends ViewGroup.LayoutParams {
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
- }
-
- /**
- * For {@link TagGroup} save and restore state.
- */
- static class SavedState extends BaseSavedState {
- public static final Parcelable.Creator CREATOR =
- new Parcelable.Creator() {
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- int tagCount;
- String[] tags;
- int checkedPosition;
- String input;
-
- public SavedState(Parcel source) {
- super(source);
- tagCount = source.readInt();
- tags = new String[tagCount];
- source.readStringArray(tags);
- checkedPosition = source.readInt();
- input = source.readString();
- }
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- tagCount = tags.length;
- dest.writeInt(tagCount);
- dest.writeStringArray(tags);
- dest.writeInt(checkedPosition);
- dest.writeString(input);
- }
- }
-
- /**
- * The tag view click listener for internal use.
- */
- class InternalTagClickListener implements OnClickListener {
- @Override
- public void onClick(View v) {
- final TagView tag = (TagView) v;
- if (isAppendMode) {
- if (tag.mState == TagView.STATE_INPUT) {
- // If the clicked tag is in INPUT state, uncheck the previous checked tag if exists.
- final TagView checkedTag = getCheckedTag();
- if (checkedTag != null) {
- checkedTag.setChecked(false);
- }
- } else {
- // If the clicked tag is currently checked, delete the tag.
- if (tag.isChecked) {
- deleteTag(tag);
- } else {
- // If the clicked tag is unchecked, uncheck the previous checked tag if exists,
- // then check the clicked tag.
- final TagView checkedTag = getCheckedTag();
- if (checkedTag != null) {
- checkedTag.setChecked(false);
- }
- tag.setChecked(true);
- }
- }
- } else {
- if (mOnTagClickListener != null) {
- mOnTagClickListener.onTagClick(tag.getText().toString());
- }
- }
- }
- }
-
- /**
- * The tag view which has two states can be either NORMAL or INPUT.
- */
- class TagView extends TextView {
- public static final int STATE_NORMAL = 1;
- public static final int STATE_INPUT = 2;
-
- /** The offset to the text. */
- private static final int CHECKED_MARKER_OFFSET = 3;
-
- /** The stroke width of the checked marker */
- private static final int CHECKED_MARKER_STROKE_WIDTH = 4;
-
- /** The current state. */
- private int mState;
-
- /** Indicates the tag if checked. */
- private boolean isChecked = false;
-
- /** Indicates the tag if pressed. */
- private boolean isPressed = false;
-
- private Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- private Paint mCheckedMarkerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- /** The rect for the tag's left corner drawing. */
- private RectF mLeftCornerRectF = new RectF();
-
- /** The rect for the tag's right corner drawing. */
- private RectF mRightCornerRectF = new RectF();
-
- /** The rect for the tag's horizontal blank fill area. */
- private RectF mHorizontalBlankFillRectF = new RectF();
-
- /** The rect for the tag's vertical blank fill area. */
- private RectF mVerticalBlankFillRectF = new RectF();
-
- /** The rect for the checked mark draw bound. */
- private RectF mCheckedMarkerBound = new RectF();
-
- /** Used to detect the touch event. */
- private Rect mOutRect = new Rect();
-
- /** The path for draw the tag's outline border. */
- private Path mBorderPath = new Path();
-
- /** The path effect provide draw the dash border. */
- private PathEffect mPathEffect = new DashPathEffect(new float[]{10, 5}, 0);
-
- {
- mBorderPaint.setStyle(Paint.Style.STROKE);
- mBorderPaint.setStrokeWidth(borderStrokeWidth);
- mBackgroundPaint.setStyle(Paint.Style.FILL);
- mCheckedMarkerPaint.setStyle(Paint.Style.FILL);
- mCheckedMarkerPaint.setStrokeWidth(CHECKED_MARKER_STROKE_WIDTH);
- mCheckedMarkerPaint.setColor(checkedMarkerColor);
- }
-
-
- public TagView(Context context, final int state, CharSequence text) {
- super(context);
- setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
- setLayoutParams(new TagGroup.LayoutParams(
- TagGroup.LayoutParams.WRAP_CONTENT,
- TagGroup.LayoutParams.WRAP_CONTENT));
-
- setGravity(Gravity.CENTER);
- setText(text);
- setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
-
- mState = state;
-
- setClickable(isAppendMode);
- setFocusable(state == STATE_INPUT);
- setFocusableInTouchMode(state == STATE_INPUT);
- setHint(state == STATE_INPUT ? inputHint : null);
- setMovementMethod(state == STATE_INPUT ? ArrowKeyMovementMethod.getInstance() : null);
-
- // Interrupted long click event to avoid PAUSE popup.
- setOnLongClickListener(new OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- return state != STATE_INPUT;
- }
- });
-
- if (state == STATE_INPUT) {
- requestFocus();
-
- // Handle the ENTER key down.
- setOnEditorActionListener(new OnEditorActionListener() {
- @Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if (actionId == EditorInfo.IME_NULL
- && (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
- && event.getAction() == KeyEvent.ACTION_DOWN)) {
- if (isInputAvailable()) {
- // If the input content is available, end the input and dispatch
- // the event, then append a new INPUT state tag.
- endInput();
- if (mOnTagChangeListener != null) {
- mOnTagChangeListener.onAppend(TagGroup.this, getText().toString());
- }
- appendInputTag();
- }
- return true;
- }
- return false;
- }
- });
-
- // Handle the BACKSPACE key down.
- setOnKeyListener(new OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
- // If the input content is empty, check or remove the last NORMAL state tag.
- if (TextUtils.isEmpty(getText().toString())) {
- TagView lastNormalTagView = getLastNormalTagView();
- if (lastNormalTagView != null) {
- if (lastNormalTagView.isChecked) {
- removeView(lastNormalTagView);
- if (mOnTagChangeListener != null) {
- mOnTagChangeListener.onDelete(TagGroup.this, lastNormalTagView.getText().toString());
- }
- } else {
- final TagView checkedTagView = getCheckedTag();
- if (checkedTagView != null) {
- checkedTagView.setChecked(false);
- }
- lastNormalTagView.setChecked(true);
- }
- return true;
- }
- }
- }
- return false;
- }
- });
-
- // Handle the INPUT tag content changed.
- addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // When the INPUT state tag changed, uncheck the checked tag if exists.
- final TagView checkedTagView = getCheckedTag();
- if (checkedTagView != null) {
- checkedTagView.setChecked(false);
- }
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- }
- });
- }
-
- invalidatePaint();
- }
-
- /**
- * Set whether this tag view is in the checked state.
- *
- * @param checked true is checked, false otherwise
- */
- public void setChecked(boolean checked) {
- isChecked = checked;
- // Make the checked mark drawing region.
- setPadding(horizontalPadding,
- verticalPadding,
- isChecked ? (int) (horizontalPadding + getHeight() / 2.5f + CHECKED_MARKER_OFFSET)
- : horizontalPadding,
- verticalPadding);
- invalidatePaint();
- }
-
- /**
- * Call this method to end this tag's INPUT state.
- */
- public void endInput() {
- // Make the view not focusable.
- setFocusable(false);
- setFocusableInTouchMode(false);
- // Set the hint empty, make the TextView measure correctly.
- setHint(null);
- // Take away the cursor.
- setMovementMethod(null);
-
- mState = STATE_NORMAL;
- invalidatePaint();
- requestLayout();
- }
-
- @Override
- protected boolean getDefaultEditable() {
- return true;
- }
-
- /**
- * Indicates whether the input content is available.
- *
- * @return True if the input content is available, false otherwise.
- */
- public boolean isInputAvailable() {
- return getText() != null && getText().length() > 0;
- }
-
- private void invalidatePaint() {
- if (isAppendMode) {
- if (mState == STATE_INPUT) {
- mBorderPaint.setColor(dashBorderColor);
- mBorderPaint.setPathEffect(mPathEffect);
- mBackgroundPaint.setColor(backgroundColor);
- setHintTextColor(inputHintColor);
- setTextColor(inputTextColor);
- } else {
- mBorderPaint.setPathEffect(null);
- if (isChecked) {
- mBorderPaint.setColor(checkedBorderColor);
- mBackgroundPaint.setColor(checkedBackgroundColor);
- setTextColor(checkedTextColor);
- } else {
- mBorderPaint.setColor(borderColor);
- mBackgroundPaint.setColor(backgroundColor);
- setTextColor(textColor);
- }
- }
- } else {
- mBorderPaint.setColor(borderColor);
- mBackgroundPaint.setColor(backgroundColor);
- setTextColor(textColor);
- }
-
- if (isPressed) {
- mBackgroundPaint.setColor(pressedBackgroundColor);
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawArc(mLeftCornerRectF, -180, 90, true, mBackgroundPaint);
- canvas.drawArc(mLeftCornerRectF, -270, 90, true, mBackgroundPaint);
- canvas.drawArc(mRightCornerRectF, -90, 90, true, mBackgroundPaint);
- canvas.drawArc(mRightCornerRectF, 0, 90, true, mBackgroundPaint);
- canvas.drawRect(mHorizontalBlankFillRectF, mBackgroundPaint);
- canvas.drawRect(mVerticalBlankFillRectF, mBackgroundPaint);
-
- if (isChecked) {
- canvas.save();
- canvas.rotate(45, mCheckedMarkerBound.centerX(), mCheckedMarkerBound.centerY());
- canvas.drawLine(mCheckedMarkerBound.left, mCheckedMarkerBound.centerY(),
- mCheckedMarkerBound.right, mCheckedMarkerBound.centerY(), mCheckedMarkerPaint);
- canvas.drawLine(mCheckedMarkerBound.centerX(), mCheckedMarkerBound.top,
- mCheckedMarkerBound.centerX(), mCheckedMarkerBound.bottom, mCheckedMarkerPaint);
- canvas.restore();
- }
- canvas.drawPath(mBorderPath, mBorderPaint);
- super.onDraw(canvas);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- int left = (int) borderStrokeWidth;
- int top = (int) borderStrokeWidth;
- int right = (int) (left + w - borderStrokeWidth * 2);
- int bottom = (int) (top + h - borderStrokeWidth * 2);
-
- int d = bottom - top;
-
- mLeftCornerRectF.set(left, top, left + d, top + d);
- mRightCornerRectF.set(right - d, top, right, top + d);
-
- mBorderPath.reset();
- mBorderPath.addArc(mLeftCornerRectF, -180, 90);
- mBorderPath.addArc(mLeftCornerRectF, -270, 90);
- mBorderPath.addArc(mRightCornerRectF, -90, 90);
- mBorderPath.addArc(mRightCornerRectF, 0, 90);
-
- int l = (int) (d / 2.0f);
- mBorderPath.moveTo(left + l, top);
- mBorderPath.lineTo(right - l, top);
-
- mBorderPath.moveTo(left + l, bottom);
- mBorderPath.lineTo(right - l, bottom);
-
- mBorderPath.moveTo(left, top + l);
- mBorderPath.lineTo(left, bottom - l);
-
- mBorderPath.moveTo(right, top + l);
- mBorderPath.lineTo(right, bottom - l);
-
- mHorizontalBlankFillRectF.set(left, top + l, right, bottom - l);
- mVerticalBlankFillRectF.set(left + l, top, right - l, bottom);
-
- int m = (int) (h / 2.5f);
- h = bottom - top;
- mCheckedMarkerBound.set(right - m - horizontalPadding + CHECKED_MARKER_OFFSET,
- top + h / 2 - m / 2,
- right - horizontalPadding + CHECKED_MARKER_OFFSET,
- bottom - h / 2 + m / 2);
-
- // Ensure the checked mark drawing region is correct across screen orientation changes.
- if (isChecked) {
- setPadding(horizontalPadding,
- verticalPadding,
- (int) (horizontalPadding + h / 2.5f + CHECKED_MARKER_OFFSET),
- verticalPadding);
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (mState == STATE_INPUT) {
- // The INPUT tag doesn't change background color on the touch event.
- return super.onTouchEvent(event);
- }
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- getDrawingRect(mOutRect);
- isPressed = true;
- invalidatePaint();
- invalidate();
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- if (!mOutRect.contains((int) event.getX(), (int) event.getY())) {
- isPressed = false;
- invalidatePaint();
- invalidate();
- }
- break;
- }
- case MotionEvent.ACTION_UP: {
- isPressed = false;
- invalidatePaint();
- invalidate();
- break;
- }
- }
- return super.onTouchEvent(event);
- }
-
- @Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- return new ZanyInputConnection(super.onCreateInputConnection(outAttrs), true);
- }
-
- /**
- * Solve edit text delete(backspace) key detect, see
- * Android: Backspace in WebView/BaseInputConnection
- */
- private class ZanyInputConnection extends InputConnectionWrapper {
- public ZanyInputConnection(android.view.inputmethod.InputConnection target, boolean mutable) {
- super(target, mutable);
- }
-
- @Override
- public boolean deleteSurroundingText(int beforeLength, int afterLength) {
- // magic: in latest Android, deleteSurroundingText(1, 0) will be called for backspace
- if (beforeLength == 1 && afterLength == 0) {
- // backspace
- return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
- && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
- }
- return super.deleteSurroundingText(beforeLength, afterLength);
- }
- }
- }
+package me.gujun.android.taggroup;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.ArrowKeyMovementMethod;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A TagGroup
is a special layout with a set of tags.
+ * This group has two modes:
+ *
+ * 1. APPEND mode
+ * 2. DISPLAY mode
+ *
+ * Default is DISPLAY mode. When in APPEND mode, the group is capable of input for append new tags
+ * and delete tags.
+ *
+ * When in DISPLAY mode, the group is only contain NORMAL state tags, and the tags in group
+ * is not focusable.
+ *
+ *
+ * @author Jun Gu (http://2dxgujun.com)
+ * @version 2.0
+ * @since 2015-2-3 14:16:32
+ */
+public class TagGroup extends ViewGroup {
+ private final int default_border_color = Color.rgb(0x49, 0xC1, 0x20);
+ private final int default_text_color = Color.rgb(0x49, 0xC1, 0x20);
+ private final int default_background_color = Color.WHITE;
+ private final int default_dash_border_color = Color.rgb(0xAA, 0xAA, 0xAA);
+ private final int default_input_hint_color = Color.argb(0x80, 0x00, 0x00, 0x00);
+ private final int default_input_text_color = Color.argb(0xDE, 0x00, 0x00, 0x00);
+ private final int default_checked_border_color = Color.rgb(0x49, 0xC1, 0x20);
+ private final int default_checked_text_color = Color.WHITE;
+ private final int default_checked_marker_color = Color.WHITE;
+ private final int default_checked_background_color = Color.rgb(0x49, 0xC1, 0x20);
+ private final int default_pressed_background_color = Color.rgb(0xED, 0xED, 0xED);
+ private final float default_border_stroke_width;
+ private final float default_text_size;
+ private final float default_horizontal_spacing;
+ private final float default_vertical_spacing;
+ private final float default_horizontal_padding;
+ private final float default_vertical_padding;
+
+ /**
+ * Indicates whether this TagGroup is set up to APPEND mode or DISPLAY mode. Default is false.
+ */
+ private boolean isAppendMode;
+
+ /**
+ * The text to be displayed when the text of the INPUT tag is empty.
+ */
+ private CharSequence inputHint;
+
+ /**
+ * The tag outline border color.
+ */
+ private int borderColor;
+
+ /**
+ * The tag text color.
+ */
+ private int textColor;
+
+ /**
+ * The tag background color.
+ */
+ private int backgroundColor;
+
+ /**
+ * The dash outline border color.
+ */
+ private int dashBorderColor;
+
+ /**
+ * The input tag hint text color.
+ */
+ private int inputHintColor;
+
+ /**
+ * The input tag type text color.
+ */
+ private int inputTextColor;
+
+ /**
+ * The checked tag outline border color.
+ */
+ private int checkedBorderColor;
+
+ /**
+ * The check text color
+ */
+ private int checkedTextColor;
+
+ /**
+ * The checked marker color.
+ */
+ private int checkedMarkerColor;
+
+ /**
+ * The checked tag background color.
+ */
+ private int checkedBackgroundColor;
+
+ /**
+ * The tag background color, when the tag is being pressed.
+ */
+ private int pressedBackgroundColor;
+
+ /**
+ * The tag outline border stroke width, default is 0.5dp.
+ */
+ private float borderStrokeWidth;
+
+ /**
+ * The tag text size, default is 13sp.
+ */
+ private float textSize;
+
+ /**
+ * The horizontal tag spacing, default is 8.0dp.
+ */
+ private int horizontalSpacing;
+
+ /**
+ * The vertical tag spacing, default is 4.0dp.
+ */
+ private int verticalSpacing;
+
+ /**
+ * The horizontal tag padding, default is 12.0dp.
+ */
+ private int horizontalPadding;
+
+ /**
+ * The vertical tag padding, default is 3.0dp.
+ */
+ private int verticalPadding;
+
+ /**
+ * Listener used to dispatch tag change event.
+ */
+ private OnTagChangeListener mOnTagChangeListener;
+
+ /**
+ * Listener used to dispatch tag click event.
+ */
+ private OnTagClickListener mOnTagClickListener;
+
+ /**
+ * Listener used to handle tag click event.
+ */
+ private InternalTagClickListener mInternalTagClickListener = new InternalTagClickListener();
+
+ public TagGroup(Context context) {
+ this(context, null);
+ }
+
+ public TagGroup(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.tagGroupStyle);
+ }
+
+ public TagGroup(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ default_border_stroke_width = dp2px(0.5f);
+ default_text_size = sp2px(13.0f);
+ default_horizontal_spacing = dp2px(8.0f);
+ default_vertical_spacing = dp2px(4.0f);
+ default_horizontal_padding = dp2px(12.0f);
+ default_vertical_padding = dp2px(3.0f);
+
+ // Load styled attributes.
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TagGroup, defStyleAttr, R.style.TagGroup);
+ try {
+ isAppendMode = a.getBoolean(R.styleable.TagGroup_atg_isAppendMode, false);
+ inputHint = a.getText(R.styleable.TagGroup_atg_inputHint);
+ borderColor = a.getColor(R.styleable.TagGroup_atg_borderColor, default_border_color);
+ textColor = a.getColor(R.styleable.TagGroup_atg_textColor, default_text_color);
+ backgroundColor = a.getColor(R.styleable.TagGroup_atg_backgroundColor, default_background_color);
+ dashBorderColor = a.getColor(R.styleable.TagGroup_atg_dashBorderColor, default_dash_border_color);
+ inputHintColor = a.getColor(R.styleable.TagGroup_atg_inputHintColor, default_input_hint_color);
+ inputTextColor = a.getColor(R.styleable.TagGroup_atg_inputTextColor, default_input_text_color);
+ checkedBorderColor = a.getColor(R.styleable.TagGroup_atg_checkedBorderColor, default_checked_border_color);
+ checkedTextColor = a.getColor(R.styleable.TagGroup_atg_checkedTextColor, default_checked_text_color);
+ checkedMarkerColor = a.getColor(R.styleable.TagGroup_atg_checkedMarkerColor, default_checked_marker_color);
+ checkedBackgroundColor = a.getColor(R.styleable.TagGroup_atg_checkedBackgroundColor, default_checked_background_color);
+ pressedBackgroundColor = a.getColor(R.styleable.TagGroup_atg_pressedBackgroundColor, default_pressed_background_color);
+ borderStrokeWidth = a.getDimension(R.styleable.TagGroup_atg_borderStrokeWidth, default_border_stroke_width);
+ textSize = a.getDimension(R.styleable.TagGroup_atg_textSize, default_text_size);
+ horizontalSpacing = (int) a.getDimension(R.styleable.TagGroup_atg_horizontalSpacing, default_horizontal_spacing);
+ verticalSpacing = (int) a.getDimension(R.styleable.TagGroup_atg_verticalSpacing, default_vertical_spacing);
+ horizontalPadding = (int) a.getDimension(R.styleable.TagGroup_atg_horizontalPadding, default_horizontal_padding);
+ verticalPadding = (int) a.getDimension(R.styleable.TagGroup_atg_verticalPadding, default_vertical_padding);
+ } finally {
+ a.recycle();
+ }
+
+ if (isAppendMode) {
+ // Append the initial INPUT tag.
+ appendInputTag();
+
+ // Set the click listener to detect the end-input event.
+ setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submitTag();
+ }
+ });
+ }
+ }
+
+ /**
+ * Call this to submit the INPUT tag.
+ */
+ public void submitTag() {
+ final TagView inputTag = getInputTag();
+ if (inputTag != null && inputTag.isInputAvailable()) {
+ inputTag.endInput();
+
+ if (mOnTagChangeListener != null) {
+ mOnTagChangeListener.onAppend(TagGroup.this, inputTag.getText().toString());
+ }
+ appendInputTag();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+
+ int width = 0;
+ int height = 0;
+
+ int row = 0; // The row counter.
+ int rowWidth = 0; // Calc the current row width.
+ int rowMaxHeight = 0; // Calc the max tag height, in current row.
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ if (child.getVisibility() != GONE) {
+ rowWidth += childWidth;
+ if (rowWidth > widthSize) { // Next line.
+ rowWidth = childWidth; // The next row width.
+ height += rowMaxHeight + verticalSpacing;
+ rowMaxHeight = childHeight; // The next row max height.
+ row++;
+ } else { // This line.
+ rowMaxHeight = Math.max(rowMaxHeight, childHeight);
+ }
+ rowWidth += horizontalSpacing;
+ }
+ }
+ // Account for the last row height.
+ height += rowMaxHeight;
+
+ // Account for the padding too.
+ height += getPaddingTop() + getPaddingBottom();
+
+ // If the tags grouped in one row, set the width to wrap the tags.
+ if (row == 0) {
+ width = rowWidth;
+ width += getPaddingLeft() + getPaddingRight();
+ } else {// If the tags grouped exceed one line, set the width to match the parent.
+ width = widthSize;
+ }
+
+ setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
+ heightMode == MeasureSpec.EXACTLY ? heightSize : height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int parentLeft = getPaddingLeft();
+ final int parentRight = r - l - getPaddingRight();
+ final int parentTop = getPaddingTop();
+ final int parentBottom = b - t - getPaddingBottom();
+
+ int childLeft = parentLeft;
+ int childTop = parentTop;
+
+ int rowMaxHeight = 0;
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ final int width = child.getMeasuredWidth();
+ final int height = child.getMeasuredHeight();
+
+ if (child.getVisibility() != GONE) {
+ if (childLeft + width > parentRight) { // Next line
+ childLeft = parentLeft;
+ childTop += rowMaxHeight + verticalSpacing;
+ rowMaxHeight = height;
+ } else {
+ rowMaxHeight = Math.max(rowMaxHeight, height);
+ }
+ child.layout(childLeft, childTop, childLeft + width, childTop + height);
+
+ childLeft += width + horizontalSpacing;
+ }
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.tags = getTags();
+ ss.checkedPosition = getCheckedTagIndex();
+ if (getInputTag() != null) {
+ ss.input = getInputTag().getText().toString();
+ }
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ setTags(ss.tags);
+ TagView checkedTagView = getTagAt(ss.checkedPosition);
+ if (checkedTagView != null) {
+ checkedTagView.setChecked(true);
+ }
+ if (getInputTag() != null) {
+ getInputTag().setText(ss.input);
+ }
+ }
+
+ /**
+ * Returns the INPUT tag view in this group.
+ *
+ * @return the INPUT state tag view or null if not exists
+ */
+ protected TagView getInputTag() {
+ if (isAppendMode) {
+ final int inputTagIndex = getChildCount() - 1;
+ final TagView inputTag = getTagAt(inputTagIndex);
+ if (inputTag != null && inputTag.mState == TagView.STATE_INPUT) {
+ return inputTag;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the INPUT state tag in this group.
+ *
+ * @return the INPUT state tag view or null if not exists
+ */
+ public String getInputTagText() {
+ final TagView inputTagView = getInputTag();
+ if (inputTagView != null) {
+ return inputTagView.getText().toString();
+ }
+ return null;
+ }
+
+ /**
+ * Return the last NORMAL state tag view in this group.
+ *
+ * @return the last NORMAL state tag view or null if not exists
+ */
+ protected TagView getLastNormalTagView() {
+ final int lastNormalTagIndex = isAppendMode ? getChildCount() - 2 : getChildCount() - 1;
+ TagView lastNormalTagView = getTagAt(lastNormalTagIndex);
+ return lastNormalTagView;
+ }
+
+ /**
+ * Returns the tag array in group, except the INPUT tag.
+ *
+ * @return the tag array.
+ */
+ public String[] getTags() {
+ final int count = getChildCount();
+ final List tagList = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ final TagView tagView = getTagAt(i);
+ if (tagView.mState == TagView.STATE_NORMAL) {
+ tagList.add(tagView.getText().toString());
+ }
+ }
+
+ return tagList.toArray(new String[tagList.size()]);
+ }
+
+ /**
+ * @see #setTags(String...)
+ */
+ public void setTags(List tagList) {
+ setTags(tagList.toArray(new String[tagList.size()]));
+ }
+
+ /**
+ * Set the tags. It will remove all previous tags first.
+ *
+ * @param tags the tag list to set.
+ */
+ public void setTags(String... tags) {
+ removeAllViews();
+ for (final String tag : tags) {
+ appendTag(tag);
+ }
+
+ if (isAppendMode) {
+ appendInputTag();
+ }
+ }
+
+ /**
+ * Returns the tag view at the specified position in the group.
+ *
+ * @param index the position at which to get the tag view from.
+ * @return the tag view at the specified position or null if the position
+ * does not exists within this group.
+ */
+ protected TagView getTagAt(int index) {
+ return (TagView) getChildAt(index);
+ }
+
+ /**
+ * Returns the checked tag view in the group.
+ *
+ * @return the checked tag view or null if not exists.
+ */
+ protected TagView getCheckedTag() {
+ final int checkedTagIndex = getCheckedTagIndex();
+ if (checkedTagIndex != -1) {
+ return getTagAt(checkedTagIndex);
+ }
+ return null;
+ }
+
+ /**
+ * Return the checked tag index.
+ *
+ * @return the checked tag index, or -1 if not exists.
+ */
+ protected int getCheckedTagIndex() {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final TagView tag = getTagAt(i);
+ if (tag.isChecked) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Register a callback to be invoked when this tag group is changed.
+ *
+ * @param l the callback that will run
+ */
+ public void setOnTagChangeListener(OnTagChangeListener l) {
+ mOnTagChangeListener = l;
+ }
+
+ /**
+ * @see #appendInputTag(String)
+ */
+ protected void appendInputTag() {
+ appendInputTag(null);
+ }
+
+ /**
+ * Append a INPUT tag to this group. It will throw an exception if there has a previous INPUT tag.
+ *
+ * @param tag the tag text.
+ */
+ protected void appendInputTag(String tag) {
+ final TagView previousInputTag = getInputTag();
+ if (previousInputTag != null) {
+ throw new IllegalStateException("Already has a INPUT tag in group.");
+ }
+
+ final TagView newInputTag = new TagView(getContext(), TagView.STATE_INPUT, tag);
+ newInputTag.setOnClickListener(mInternalTagClickListener);
+ addView(newInputTag);
+ }
+
+ /**
+ * Append tag to this group.
+ *
+ * @param tag the tag to append.
+ */
+ protected void appendTag(CharSequence tag) {
+ final TagView newTag = new TagView(getContext(), TagView.STATE_NORMAL, tag);
+ newTag.setOnClickListener(mInternalTagClickListener);
+ addView(newTag);
+ }
+
+ public float dp2px(float dp) {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+ getResources().getDisplayMetrics());
+ }
+
+ public float sp2px(float sp) {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
+ getResources().getDisplayMetrics());
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * Register a callback to be invoked when a tag is clicked.
+ *
+ * @param l the callback that will run.
+ */
+ public void setOnTagClickListener(OnTagClickListener l) {
+ mOnTagClickListener = l;
+ }
+
+ protected void deleteTag(TagView tagView) {
+ removeView(tagView);
+ if (mOnTagChangeListener != null) {
+ mOnTagChangeListener.onDelete(TagGroup.this, tagView.getText().toString());
+ }
+ }
+
+ /**
+ * Change the current state to APPEND MODEL or not
+ *
+ * @param isAppendMode
+ */
+ public void setAppendModel(boolean isAppendMode) {
+ this.isAppendMode = isAppendMode;
+ if (isAppendMode) {
+ // Append the initial INPUT tag.
+ appendInputTag();
+ // Set the click listener to detect the end-input event.
+ setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submitTag();
+ }
+ });
+ invalidate();
+ } else {
+ int lastIndex = getChildCount() - 1;
+ if (lastIndex >= 0) {
+ TagView inputTag = getTagAt(lastIndex);
+ if (inputTag != null && inputTag.mState == TagView.STATE_INPUT) {
+ // remove the listener
+ setOnClickListener(null);
+ // remove the INPUT tag
+ removeViewAt(getChildCount() - 1);
+ } else {
+ //do nothing
+ }
+ } else {
+ //do nothing
+ }
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a tag group is changed.
+ */
+ public interface OnTagChangeListener {
+ /**
+ * Called when a tag has been appended to the group.
+ *
+ * @param tag the appended tag.
+ */
+ void onAppend(TagGroup tagGroup, String tag);
+
+ /**
+ * Called when a tag has been deleted from the the group.
+ *
+ * @param tag the deleted tag.
+ */
+ void onDelete(TagGroup tagGroup, String tag);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a tag is clicked.
+ */
+ public interface OnTagClickListener {
+ /**
+ * Called when a tag has been clicked.
+ *
+ * @param tag The tag text of the tag that was clicked.
+ */
+ void onTagClick(String tag);
+ }
+
+ /**
+ * Per-child layout information for layouts.
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+ }
+
+ /**
+ * For {@link TagGroup} save and restore state.
+ */
+ static class SavedState extends BaseSavedState {
+ public static final Creator CREATOR =
+ new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ int tagCount;
+ String[] tags;
+ int checkedPosition;
+ String input;
+
+ public SavedState(Parcel source) {
+ super(source);
+ tagCount = source.readInt();
+ tags = new String[tagCount];
+ source.readStringArray(tags);
+ checkedPosition = source.readInt();
+ input = source.readString();
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ tagCount = tags.length;
+ dest.writeInt(tagCount);
+ dest.writeStringArray(tags);
+ dest.writeInt(checkedPosition);
+ dest.writeString(input);
+ }
+ }
+
+ /**
+ * The tag view click listener for internal use.
+ */
+ class InternalTagClickListener implements OnClickListener {
+ @Override
+ public void onClick(View v) {
+ final TagView tag = (TagView) v;
+ if (isAppendMode) {
+ if (tag.mState == TagView.STATE_INPUT) {
+ // If the clicked tag is in INPUT state, uncheck the previous checked tag if exists.
+ final TagView checkedTag = getCheckedTag();
+ if (checkedTag != null) {
+ checkedTag.setChecked(false);
+ }
+ } else {
+ // If the clicked tag is currently checked, delete the tag.
+ if (tag.isChecked) {
+ deleteTag(tag);
+ } else {
+ // If the clicked tag is unchecked, uncheck the previous checked tag if exists,
+ // then check the clicked tag.
+ final TagView checkedTag = getCheckedTag();
+ if (checkedTag != null) {
+ checkedTag.setChecked(false);
+ }
+ tag.setChecked(true);
+ }
+ }
+ } else {
+ if (mOnTagClickListener != null) {
+ mOnTagClickListener.onTagClick(tag.getText().toString());
+ }
+ }
+ }
+ }
+
+
+ /**
+ * The tag view which has two states can be either NORMAL or INPUT.
+ */
+ class TagView extends TextView {
+ public static final int STATE_NORMAL = 1;
+ public static final int STATE_INPUT = 2;
+
+ /**
+ * The offset to the text.
+ */
+ private static final int CHECKED_MARKER_OFFSET = 3;
+
+ /**
+ * The stroke width of the checked marker
+ */
+ private static final int CHECKED_MARKER_STROKE_WIDTH = 4;
+
+ /**
+ * The current state.
+ */
+ private int mState;
+
+ /**
+ * Indicates the tag if checked.
+ */
+ private boolean isChecked = false;
+
+ /**
+ * Indicates the tag if pressed.
+ */
+ private boolean isPressed = false;
+
+ private Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private Paint mCheckedMarkerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ /**
+ * The rect for the tag's left corner drawing.
+ */
+ private RectF mLeftCornerRectF = new RectF();
+
+ /**
+ * The rect for the tag's right corner drawing.
+ */
+ private RectF mRightCornerRectF = new RectF();
+
+ /**
+ * The rect for the tag's horizontal blank fill area.
+ */
+ private RectF mHorizontalBlankFillRectF = new RectF();
+
+ /**
+ * The rect for the tag's vertical blank fill area.
+ */
+ private RectF mVerticalBlankFillRectF = new RectF();
+
+ /**
+ * The rect for the checked mark draw bound.
+ */
+ private RectF mCheckedMarkerBound = new RectF();
+
+ /**
+ * Used to detect the touch event.
+ */
+ private Rect mOutRect = new Rect();
+
+ /**
+ * The path for draw the tag's outline border.
+ */
+ private Path mBorderPath = new Path();
+
+ /**
+ * The path effect provide draw the dash border.
+ */
+ private PathEffect mPathEffect = new DashPathEffect(new float[]{10, 5}, 0);
+
+ {
+ mBorderPaint.setStyle(Paint.Style.STROKE);
+ mBorderPaint.setStrokeWidth(borderStrokeWidth);
+ mBackgroundPaint.setStyle(Paint.Style.FILL);
+ mCheckedMarkerPaint.setStyle(Paint.Style.FILL);
+ mCheckedMarkerPaint.setStrokeWidth(CHECKED_MARKER_STROKE_WIDTH);
+ mCheckedMarkerPaint.setColor(checkedMarkerColor);
+ }
+
+
+ public TagView(Context context, final int state, CharSequence text) {
+ super(context);
+ setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
+ setLayoutParams(new LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+
+ setGravity(Gravity.CENTER);
+ setText(text);
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+
+ mState = state;
+
+ setClickable(isAppendMode);
+ setFocusable(state == STATE_INPUT);
+ setFocusableInTouchMode(state == STATE_INPUT);
+ setHint(state == STATE_INPUT ? inputHint : null);
+ setMovementMethod(state == STATE_INPUT ? ArrowKeyMovementMethod.getInstance() : null);
+
+ // Interrupted long click event to avoid PAUSE popup.
+ setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ return state != STATE_INPUT;
+ }
+ });
+
+ if (state == STATE_INPUT) {
+ requestFocus();
+
+ // Handle the ENTER key down.
+ setOnEditorActionListener(new OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_NULL
+ && (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
+ && event.getAction() == KeyEvent.ACTION_DOWN)) {
+ if (isInputAvailable()) {
+ // If the input content is available, end the input and dispatch
+ // the event, then append a new INPUT state tag.
+ endInput();
+ if (mOnTagChangeListener != null) {
+ mOnTagChangeListener.onAppend(TagGroup.this, getText().toString());
+ }
+ appendInputTag();
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+
+ // Handle the BACKSPACE key down.
+ setOnKeyListener(new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
+ // If the input content is empty, check or remove the last NORMAL state tag.
+ if (TextUtils.isEmpty(getText().toString())) {
+ TagView lastNormalTagView = getLastNormalTagView();
+ if (lastNormalTagView != null) {
+ if (lastNormalTagView.isChecked) {
+ removeView(lastNormalTagView);
+ if (mOnTagChangeListener != null) {
+ mOnTagChangeListener.onDelete(TagGroup.this, lastNormalTagView.getText().toString());
+ }
+ } else {
+ final TagView checkedTagView = getCheckedTag();
+ if (checkedTagView != null) {
+ checkedTagView.setChecked(false);
+ }
+ lastNormalTagView.setChecked(true);
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ });
+
+ // Handle the INPUT tag content changed.
+ addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // When the INPUT state tag changed, uncheck the checked tag if exists.
+ final TagView checkedTagView = getCheckedTag();
+ if (checkedTagView != null) {
+ checkedTagView.setChecked(false);
+ }
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ invalidatePaint();
+ }
+
+ /**
+ * Set whether this tag view is in the checked state.
+ *
+ * @param checked true is checked, false otherwise
+ */
+ public void setChecked(boolean checked) {
+ isChecked = checked;
+ // Make the checked mark drawing region.
+ if (isChecked) setPadding(horizontalPadding,
+ verticalPadding,
+ (int) (horizontalPadding + getHeight() / 2.5f + CHECKED_MARKER_OFFSET),
+ verticalPadding);
+ else setPadding(horizontalPadding,
+ verticalPadding,
+ horizontalPadding,
+ verticalPadding);
+ invalidatePaint();
+ }
+
+ /**
+ * Call this method to end this tag's INPUT state.
+ */
+ public void endInput() {
+ // Make the view not focusable.
+ setFocusable(false);
+ setFocusableInTouchMode(false);
+ // Set the hint empty, make the TextView measure correctly.
+ setHint(null);
+ // Take away the cursor.
+ setMovementMethod(null);
+
+ mState = STATE_NORMAL;
+ invalidatePaint();
+ requestLayout();
+ }
+
+ @Override
+ protected boolean getDefaultEditable() {
+ return true;
+ }
+
+ /**
+ * Indicates whether the input content is available.
+ *
+ * @return True if the input content is available, false otherwise.
+ */
+ public boolean isInputAvailable() {
+ return getText() != null && getText().length() > 0;
+ }
+
+ private void invalidatePaint() {
+ if (isAppendMode) {
+ if (mState == STATE_INPUT) {
+ mBorderPaint.setColor(dashBorderColor);
+ mBorderPaint.setPathEffect(mPathEffect);
+ mBackgroundPaint.setColor(backgroundColor);
+ setHintTextColor(inputHintColor);
+ setTextColor(inputTextColor);
+ } else {
+ mBorderPaint.setPathEffect(null);
+ if (isChecked) {
+ mBorderPaint.setColor(checkedBorderColor);
+ mBackgroundPaint.setColor(checkedBackgroundColor);
+ setTextColor(checkedTextColor);
+ } else {
+ mBorderPaint.setColor(borderColor);
+ mBackgroundPaint.setColor(backgroundColor);
+ setTextColor(textColor);
+ }
+ }
+ } else {
+ mBorderPaint.setColor(borderColor);
+ mBackgroundPaint.setColor(backgroundColor);
+ setTextColor(textColor);
+ }
+
+ if (isPressed) {
+ mBackgroundPaint.setColor(pressedBackgroundColor);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawArc(mLeftCornerRectF, -180, 90, true, mBackgroundPaint);
+ canvas.drawArc(mLeftCornerRectF, -270, 90, true, mBackgroundPaint);
+ canvas.drawArc(mRightCornerRectF, -90, 90, true, mBackgroundPaint);
+ canvas.drawArc(mRightCornerRectF, 0, 90, true, mBackgroundPaint);
+ canvas.drawRect(mHorizontalBlankFillRectF, mBackgroundPaint);
+ canvas.drawRect(mVerticalBlankFillRectF, mBackgroundPaint);
+
+ if (isChecked) {
+ canvas.save();
+ canvas.rotate(45, mCheckedMarkerBound.centerX(), mCheckedMarkerBound.centerY());
+ canvas.drawLine(mCheckedMarkerBound.left, mCheckedMarkerBound.centerY(),
+ mCheckedMarkerBound.right, mCheckedMarkerBound.centerY(), mCheckedMarkerPaint);
+ canvas.drawLine(mCheckedMarkerBound.centerX(), mCheckedMarkerBound.top,
+ mCheckedMarkerBound.centerX(), mCheckedMarkerBound.bottom, mCheckedMarkerPaint);
+ canvas.restore();
+ }
+ canvas.drawPath(mBorderPath, mBorderPaint);
+ super.onDraw(canvas);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ int left = (int) borderStrokeWidth;
+ int top = (int) borderStrokeWidth;
+ int right = (int) (left + w - borderStrokeWidth * 2);
+ int bottom = (int) (top + h - borderStrokeWidth * 2);
+
+ int d = bottom - top;
+
+ mLeftCornerRectF.set(left, top, left + d, top + d);
+ mRightCornerRectF.set(right - d, top, right, top + d);
+
+ mBorderPath.reset();
+ mBorderPath.addArc(mLeftCornerRectF, -180, 90);
+ mBorderPath.addArc(mLeftCornerRectF, -270, 90);
+ mBorderPath.addArc(mRightCornerRectF, -90, 90);
+ mBorderPath.addArc(mRightCornerRectF, 0, 90);
+
+ int l = (int) (d / 2.0f);
+ mBorderPath.moveTo(left + l, top);
+ mBorderPath.lineTo(right - l, top);
+
+ mBorderPath.moveTo(left + l, bottom);
+ mBorderPath.lineTo(right - l, bottom);
+
+ mBorderPath.moveTo(left, top + l);
+ mBorderPath.lineTo(left, bottom - l);
+
+ mBorderPath.moveTo(right, top + l);
+ mBorderPath.lineTo(right, bottom - l);
+
+ mHorizontalBlankFillRectF.set(left, top + l, right, bottom - l);
+ mVerticalBlankFillRectF.set(left + l, top, right - l, bottom);
+
+ int m = (int) (h / 2.5f);
+ h = bottom - top;
+ mCheckedMarkerBound.set(right - m - horizontalPadding + CHECKED_MARKER_OFFSET,
+ top + h / 2 - m / 2,
+ right - horizontalPadding + CHECKED_MARKER_OFFSET,
+ bottom - h / 2 + m / 2);
+
+ // Ensure the checked mark drawing region is correct across screen orientation changes.
+ if (isChecked) {
+ setPadding(horizontalPadding,
+ verticalPadding,
+ (int) (horizontalPadding + h / 2.5f + CHECKED_MARKER_OFFSET),
+ verticalPadding);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mState == STATE_INPUT) {
+ // The INPUT tag doesn't change background color on the touch event.
+ return super.onTouchEvent(event);
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ getDrawingRect(mOutRect);
+ isPressed = true;
+ invalidatePaint();
+ invalidate();
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (!mOutRect.contains((int) event.getX(), (int) event.getY())) {
+ isPressed = false;
+ invalidatePaint();
+ invalidate();
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ isPressed = false;
+ invalidatePaint();
+ invalidate();
+ break;
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return new ZanyInputConnection(super.onCreateInputConnection(outAttrs), true);
+ }
+
+ /**
+ * Solve edit text delete(backspace) key detect, see
+ * Android: Backspace in WebView/BaseInputConnection
+ */
+ private class ZanyInputConnection extends InputConnectionWrapper {
+ public ZanyInputConnection(InputConnection target, boolean mutable) {
+ super(target, mutable);
+ }
+
+ @Override
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ // magic: in latest Android, deleteSurroundingText(1, 0) will be called for backspace
+ if (beforeLength == 1 && afterLength == 0) {
+ // backspace
+ return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
+ && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
+ }
+ return super.deleteSurroundingText(beforeLength, afterLength);
+ }
+ }
+ }
}
\ No newline at end of file