TagGroup
is a special layout with a set of tags.
39 | * This group has two modes:
40 | * 41 | * 1. APPEND mode 42 | * 2. DISPLAY mode 43 | *
44 | * Default is DISPLAY mode. When in APPEND mode, the group is capable of input for append new tags 45 | * and delete tags. 46 | *47 | * When in DISPLAY mode, the group is only contain NORMAL state tags, and the tags in group 48 | * is not focusable. 49 | *
50 | */ 51 | public class TagsGroup extends ViewGroup { 52 | private final int default_border_color = Color.rgb(0x49, 0xC1, 0x20); 53 | private final int default_text_color = Color.rgb(0x49, 0xC1, 0x20); 54 | private final int default_background_color = Color.WHITE; 55 | private final int default_dash_border_color = Color.rgb(0xAA, 0xAA, 0xAA); 56 | private final int default_input_hint_color = Color.argb(0x80, 0x00, 0x00, 0x00); 57 | private final int default_input_text_color = Color.argb(0xDE, 0x00, 0x00, 0x00); 58 | private final int default_checked_border_color = Color.rgb(0x49, 0xC1, 0x20); 59 | private final int default_checked_text_color = Color.WHITE; 60 | private final int default_checked_marker_color = Color.WHITE; 61 | private final int default_checked_background_color = Color.rgb(0x49, 0xC1, 0x20); 62 | private final int default_selected_border_color = Color.rgb(0x49, 0xC1, 0x20); 63 | private final int default_selected_text_color = Color.WHITE; 64 | private final int default_selected_background_color = Color.rgb(0x49, 0xC1, 0x20); 65 | private final int default_pressed_background_color = Color.rgb(0xED, 0xED, 0xED); 66 | private final float default_border_stroke_width; 67 | private final float default_text_size; 68 | private final float default_horizontal_spacing; 69 | private final float default_vertical_spacing; 70 | private final float default_horizontal_padding; 71 | private final float default_vertical_padding; 72 | 73 | /** Indicates whether this TagGroup is set up to APPEND mode or DISPLAY mode. Default is false. */ 74 | private boolean isAppendMode; 75 | 76 | /** The text to be displayed when the text of the INPUT tag is empty. */ 77 | private CharSequence inputHint; 78 | 79 | /** The tag outline border color. */ 80 | private int borderColor; 81 | 82 | /** The tag text color. */ 83 | private int textColor; 84 | 85 | /** The tag background color. */ 86 | private int backgroundColor; 87 | 88 | /** The dash outline border color. */ 89 | private int dashBorderColor; 90 | 91 | /** The input tag hint text color. */ 92 | private int inputHintColor; 93 | 94 | /** The input tag type text color. */ 95 | private int inputTextColor; 96 | 97 | /** The checked tag outline border color. */ 98 | private int checkedBorderColor; 99 | 100 | /** The check text color */ 101 | private int checkedTextColor; 102 | 103 | /** The checked marker color. */ 104 | private int checkedMarkerColor; 105 | 106 | /** The checked tag background color. */ 107 | private int checkedBackgroundColor; 108 | 109 | private int selectedBorderColor; 110 | private int selectedTextColor; 111 | private int selectedBackgroundColor; 112 | 113 | /** The tag background color, when the tag is being pressed. */ 114 | private int pressedBackgroundColor; 115 | 116 | /** The tag outline border stroke width, default is 0.5dp. */ 117 | private float borderStrokeWidth; 118 | 119 | /** The tag text size, default is 13sp. */ 120 | private float textSize; 121 | 122 | /** The horizontal tag spacing, default is 8.0dp. */ 123 | private int horizontalSpacing; 124 | 125 | /** The vertical tag spacing, default is 4.0dp. */ 126 | private int verticalSpacing; 127 | 128 | /** The horizontal tag padding, default is 12.0dp. */ 129 | private int horizontalPadding; 130 | 131 | /** The vertical tag padding, default is 3.0dp. */ 132 | private int verticalPadding; 133 | 134 | private int cornerRadius; 135 | 136 | /** The max length for this tag view*/ 137 | private int tagMaxLength = 16; 138 | 139 | private int maxSize = 5; 140 | private String hint; 141 | 142 | /** Listener used to dispatch tag change event. */ 143 | private OnTagChangeListener mOnTagChangeListener; 144 | 145 | /** Listener used to dispatch tag click event. */ 146 | private OnTagClickListener mOnTagClickListener; 147 | private OnTagTextChangedListener mOnTagTextChangedListener; 148 | 149 | /** Listener used to handle tag click event. */ 150 | private InternalTagClickListener mInternalTagClickListener = new InternalTagClickListener(); 151 | 152 | public TagsGroup(Context context) { 153 | this(context, null); 154 | } 155 | 156 | public TagsGroup(Context context, AttributeSet attrs) { 157 | this(context, attrs, R.attr.tagsGroupStyle); 158 | } 159 | 160 | public TagsGroup(Context context, AttributeSet attrs, int defStyleAttr) { 161 | super(context, attrs, defStyleAttr); 162 | default_border_stroke_width = dp2px(0.5f); 163 | default_text_size = sp2px(13.0f); 164 | default_horizontal_spacing = dp2px(8.0f); 165 | default_vertical_spacing = dp2px(4.0f); 166 | default_horizontal_padding = dp2px(12.0f); 167 | default_vertical_padding = dp2px(3.0f); 168 | 169 | // Load styled attributes. 170 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TagsGroup, defStyleAttr, R.style.TagsGroup); 171 | try { 172 | isAppendMode = a.getBoolean(R.styleable.TagsGroup_tg_isAppendMode, false); 173 | inputHint = a.getText(R.styleable.TagsGroup_tg_inputHint); 174 | borderColor = a.getColor(R.styleable.TagsGroup_tg_borderColor, default_border_color); 175 | textColor = a.getColor(R.styleable.TagsGroup_tg_textColor, default_text_color); 176 | backgroundColor = a.getColor(R.styleable.TagsGroup_tg_backgroundColor, default_background_color); 177 | dashBorderColor = a.getColor(R.styleable.TagsGroup_tg_dashBorderColor, default_dash_border_color); 178 | inputHintColor = a.getColor(R.styleable.TagsGroup_tg_inputHintColor, default_input_hint_color); 179 | inputTextColor = a.getColor(R.styleable.TagsGroup_tg_inputTextColor, default_input_text_color); 180 | checkedBorderColor = a.getColor(R.styleable.TagsGroup_tg_checkedBorderColor, default_checked_border_color); 181 | checkedTextColor = a.getColor(R.styleable.TagsGroup_tg_checkedTextColor, default_checked_text_color); 182 | checkedMarkerColor = a.getColor(R.styleable.TagsGroup_tg_checkedMarkerColor, default_checked_marker_color); 183 | checkedBackgroundColor = a.getColor(R.styleable.TagsGroup_tg_checkedBackgroundColor, default_checked_background_color); 184 | selectedBorderColor = a.getColor(R.styleable.TagsGroup_tg_selectedBorderColor, default_selected_border_color); 185 | selectedTextColor = a.getColor(R.styleable.TagsGroup_tg_selectedTextColor, default_selected_text_color); 186 | selectedBackgroundColor = a.getColor(R.styleable.TagsGroup_tg_selectedBackgroundColor, default_selected_background_color); 187 | pressedBackgroundColor = a.getColor(R.styleable.TagsGroup_tg_pressedBackgroundColor, default_pressed_background_color); 188 | borderStrokeWidth = a.getDimension(R.styleable.TagsGroup_tg_borderStrokeWidth, default_border_stroke_width); 189 | textSize = a.getDimension(R.styleable.TagsGroup_tg_textSize, default_text_size); 190 | horizontalSpacing = (int) a.getDimension(R.styleable.TagsGroup_tg_horizontalSpacing, default_horizontal_spacing); 191 | verticalSpacing = (int) a.getDimension(R.styleable.TagsGroup_tg_verticalSpacing, default_vertical_spacing); 192 | horizontalPadding = (int) a.getDimension(R.styleable.TagsGroup_tg_horizontalPadding, default_horizontal_padding); 193 | verticalPadding = (int) a.getDimension(R.styleable.TagsGroup_tg_verticalPadding, default_vertical_padding); 194 | cornerRadius = a.getDimensionPixelSize(R.styleable.TagsGroup_tg_cornerRadius, -1); 195 | tagMaxLength = a.getInt(R.styleable.TagsGroup_tg_tagMaxLength, tagMaxLength); 196 | tagMaxLength = a.getInt(R.styleable.TagsGroup_tg_tagMaxLength, tagMaxLength); 197 | hint = a.getString(R.styleable.TagsGroup_tg_limitHint); 198 | maxSize = a.getInt(R.styleable.TagsGroup_tg_maxSize, maxSize); 199 | } finally { 200 | a.recycle(); 201 | } 202 | 203 | if (isAppendMode) { 204 | // Append the initial INPUT tag. 205 | appendInputTag(); 206 | 207 | // Set the click listener to detect the end-input event. 208 | setOnClickListener(new OnClickListener() { 209 | @Override 210 | public void onClick(View v) { 211 | // submitTag(); 212 | if (isAppendMode) { 213 | final TagView inputTag = getInputTag(); 214 | if (inputTag != null) { 215 | inputTag.simulateClick(); 216 | } 217 | } 218 | } 219 | }); 220 | } 221 | } 222 | 223 | protected int getSelectedTextColor() { 224 | return selectedTextColor; 225 | } 226 | 227 | /** 228 | * Call this to submit the INPUT tag. 229 | */ 230 | public void submitTag() { 231 | final TagView inputTag = getInputTag(); 232 | if (inputTag != null && inputTag.isInputAvailable()) { 233 | inputTag.endInput(); 234 | 235 | if (mOnTagChangeListener != null) { 236 | mOnTagChangeListener.onAppend(TagsGroup.this, inputTag.getText().toString().trim()); 237 | } 238 | appendInputTag(); 239 | } 240 | } 241 | 242 | @Override 243 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 244 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 245 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 246 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 247 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 248 | 249 | measureChildren(widthMeasureSpec, heightMeasureSpec); 250 | 251 | int width = 0; 252 | int height = 0; 253 | 254 | int row = 0; // The row counter. 255 | int rowWidth = 0; // Calc the current row width. 256 | int rowMaxHeight = 0; // Calc the max tag height, in current row. 257 | 258 | final int count = getChildCount(); 259 | for (int i = 0; i < count; i++) { 260 | final View child = getChildAt(i); 261 | final int childWidth = child.getMeasuredWidth(); 262 | final int childHeight = child.getMeasuredHeight(); 263 | 264 | if (child.getVisibility() != GONE) { 265 | rowWidth += childWidth; 266 | if (rowWidth > widthSize) { // Next line. 267 | rowWidth = childWidth; // The next row width. 268 | height += rowMaxHeight + verticalSpacing; 269 | rowMaxHeight = childHeight; // The next row max height. 270 | row++; 271 | } else { // This line. 272 | rowMaxHeight = Math.max(rowMaxHeight, childHeight); 273 | } 274 | rowWidth += horizontalSpacing; 275 | } 276 | } 277 | // Account for the last row height. 278 | height += rowMaxHeight; 279 | 280 | // Account for the padding too. 281 | height += getPaddingTop() + getPaddingBottom(); 282 | 283 | // If the tags grouped in one row, set the width to wrap the tags. 284 | if (row == 0) { 285 | width = rowWidth; 286 | width += getPaddingLeft() + getPaddingRight(); 287 | } else {// If the tags grouped exceed one line, set the width to match the parent. 288 | width = widthSize; 289 | } 290 | 291 | setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, 292 | heightMode == MeasureSpec.EXACTLY ? heightSize : height); 293 | } 294 | 295 | @Override 296 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 297 | final int parentLeft = getPaddingLeft(); 298 | final int parentRight = r - l - getPaddingRight(); 299 | final int parentTop = getPaddingTop(); 300 | final int parentBottom = b - t - getPaddingBottom(); 301 | 302 | int childLeft = parentLeft; 303 | int childTop = parentTop; 304 | 305 | int rowMaxHeight = 0; 306 | 307 | final int count = getChildCount(); 308 | for (int i = 0; i < count; i++) { 309 | final View child = getChildAt(i); 310 | final int width = child.getMeasuredWidth(); 311 | final int height = child.getMeasuredHeight(); 312 | 313 | if (child.getVisibility() != GONE) { 314 | if (childLeft + width > parentRight) { // Next line 315 | childLeft = parentLeft; 316 | childTop += rowMaxHeight + verticalSpacing; 317 | rowMaxHeight = height; 318 | } else { 319 | rowMaxHeight = Math.max(rowMaxHeight, height); 320 | } 321 | child.layout(childLeft, childTop, childLeft + width, childTop + height); 322 | 323 | childLeft += width + horizontalSpacing; 324 | } 325 | } 326 | } 327 | 328 | @Override 329 | public Parcelable onSaveInstanceState() { 330 | Parcelable superState = super.onSaveInstanceState(); 331 | SavedState ss = new SavedState(superState); 332 | ss.tags = getTags(); 333 | ss.checkedPosition = getCheckedTagIndex(); 334 | if (getInputTag() != null) { 335 | ss.input = getInputTag().getText().toString(); 336 | } 337 | return ss; 338 | } 339 | 340 | @Override 341 | public void onRestoreInstanceState(Parcelable state) { 342 | if (!(state instanceof SavedState)) { 343 | super.onRestoreInstanceState(state); 344 | return; 345 | } 346 | 347 | SavedState ss = (SavedState) state; 348 | super.onRestoreInstanceState(ss.getSuperState()); 349 | 350 | setTags(ss.tags); 351 | TagView checkedTagView = getTagAt(ss.checkedPosition); 352 | if (checkedTagView != null) { 353 | checkedTagView.setChecked(true); 354 | } 355 | if (getInputTag() != null) { 356 | getInputTag().setText(ss.input); 357 | } 358 | } 359 | 360 | /** 361 | * Returns the INPUT tag view in this group. 362 | * 363 | * @return the INPUT state tag view or null if not exists 364 | */ 365 | protected TagView getInputTag() { 366 | if (isAppendMode) { 367 | final int inputTagIndex = getChildCount() - 1; 368 | final TagView inputTag = getTagAt(inputTagIndex); 369 | if (inputTag != null && inputTag.mState == TagView.STATE_INPUT) { 370 | return inputTag; 371 | } else { 372 | return null; 373 | } 374 | } else { 375 | return null; 376 | } 377 | } 378 | 379 | /** 380 | * Returns the INPUT state tag in this group. 381 | * 382 | * @return the INPUT state tag view or null if not exists 383 | */ 384 | public String getInputTagText() { 385 | final TagView inputTagView = getInputTag(); 386 | if (inputTagView != null) { 387 | return inputTagView.getText().toString().trim(); 388 | } 389 | return null; 390 | } 391 | 392 | /** 393 | * Return the last NORMAL state tag view in this group. 394 | * 395 | * @return the last NORMAL state tag view or null if not exists 396 | */ 397 | protected TagView getLastNormalTagView() { 398 | final int lastNormalTagIndex = isAppendMode ? getChildCount() - 2 : getChildCount() - 1; 399 | TagView lastNormalTagView = getTagAt(lastNormalTagIndex); 400 | return lastNormalTagView; 401 | } 402 | 403 | /** 404 | * Returns the tag array in group, except the INPUT tag. 405 | * 406 | * @return the tag array. 407 | */ 408 | public String[] getTags() { 409 | final int count = getChildCount(); 410 | final List