34 | ```
35 |
36 | ```java
37 | TagGroup mTagGroup = (TagGroup) findViewById(R.id.tag_group);
38 | mTagGroup.setTags(new String[]{"Tag1", "Tag2", "Tag3"});
39 | ```
40 |
41 | Use `setTags(...)` to set the initial tags in the group.
42 |
43 | To "submit" a new tag as user press "Enter" or tap the blank area of the tag group, also you can "submit" a new tag via `submitTag()`.
44 | To delete a tag as user press "Backspace" or double-tap the tag which you want to delete.
45 |
46 | **Note**: Google keyboard (a few soft keyboard not honour the key event) currently not supported "Enter" key to "submit" a new tag and "Backspace" key to delete a tag.
47 |
48 | I made some pre-design style. You can use them via `style` property.
49 |
50 | 
51 |
52 | Use the present style just like below:
53 |
54 | ```xml
55 |
58 | ```
59 |
60 | In the above picture, the style is:
61 |
62 | `TagGroup`
63 | `TagGroup.Beauty_Red`
64 | `TagGroup.Holo_Dark`
65 | `TagGroup.Light_Blue`
66 | `TagGroup.Indigo`
67 |
68 | You can get more beautiful color from [Adobe Color CC](https://color.adobe.com), and you can also contribute your color style to AndroidTagGroup!
69 |
70 | # Build
71 |
72 | run `./gradlew assembleDebug` (Mac/Linux)
73 |
74 | or
75 |
76 | run `gradlew.bat assembleDebug` (Windows)
77 |
78 | # Attributes
79 |
80 | There are several attributes you can set:
81 |
82 | 
83 |
84 | | attr | default | mean |
85 | |:-----------------:|:----------------:|:-------------------------------------------------------:|
86 | | isAppendMode | false | Determine the TagGroup mode, APPEND or single DISPLAY. |
87 | | inputTagHint | Add Tag/添加标签 | Hint of the INPUT state tag. |
88 | | brightColor | #49C120 | The bright color of the tag. |
89 | | dimColor | #AAAAAA | The dim color of the tag. |
90 | | borderStrokeWidth | 0.5dp | The tag outline border stroke width. |
91 | | textSize | 13sp | The tag text size. |
92 | | horizontalSpacing | 8dp | The horizontal tag spacing.(Mark1) |
93 | | verticalSpacing | 4dp | The vertical tag spacing.(Mark2) |
94 | | horizontalPadding | 12dp | The horizontal tag padding.(Mark3) |
95 | | verticalPadding | 3dp | The vertical tag padding.(Mark4) |
96 |
97 | # Developed By
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | # License
107 |
108 | Copyright 2015 Jun Gu
109 |
110 | Licensed under the Apache License, Version 2.0 (the "License");
111 | you may not use this file except in compliance with the License.
112 | You may obtain a copy of the License at
113 |
114 | http://www.apache.org/licenses/LICENSE-2.0
115 |
116 | Unless required by applicable law or agreed to in writing, software
117 | distributed under the License is distributed on an "AS IS" BASIS,
118 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
119 | See the License for the specific language governing permissions and
120 | limitations under the License.
121 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/app/app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/app/src/main/java/blue/stack/grouptag/lib/TagGroup.java:
--------------------------------------------------------------------------------
1 | package blue.stack.grouptag.lib;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Canvas;
6 | import android.graphics.Color;
7 | import android.graphics.DashPathEffect;
8 | import android.graphics.Paint;
9 | import android.graphics.Path;
10 | import android.graphics.PathEffect;
11 | import android.graphics.RectF;
12 | import android.text.Editable;
13 | import android.text.TextUtils;
14 | import android.text.TextWatcher;
15 | import android.text.method.ArrowKeyMovementMethod;
16 | import android.util.AttributeSet;
17 | import android.util.TypedValue;
18 | import android.view.Gravity;
19 | import android.view.KeyEvent;
20 | import android.view.View;
21 | import android.view.ViewGroup;
22 | import android.view.inputmethod.EditorInfo;
23 | import android.widget.TextView;
24 |
25 | import java.util.ArrayList;
26 | import java.util.List;
27 |
28 | import blue.stack.grouptag.R;
29 |
30 | /**
31 | * A TagGroup is a special layout that contain a set of tags.
32 | * This group has two modes:
33 | *
34 | * 1. APPEND mode
35 | * 2. DISPLAY mode
36 | *
37 | * Default is DISPLAY mode. When in APPEND mode, the group is capable of input for append new tags
38 | * and delete tags.
39 | *
40 | * When in DISPLAY mode, the group is only contain NORMAL state tags, and the tags in group
41 | * is not focusable.
42 | *
43 | *
44 |
45 | */
46 | public class TagGroup extends ViewGroup {
47 | private final int default_bright_color = Color.rgb(0x49, 0xC1, 0x20);
48 | private final int default_dim_color = Color.rgb(0xAA, 0xAA, 0xAA);
49 | private final float default_border_stroke_width;
50 | private final float default_text_size;
51 | private final float default_horizontal_spacing;
52 | private final float default_vertical_spacing;
53 | private final float default_horizontal_padding;
54 | private final float default_vertical_padding;
55 |
56 | public boolean isAppendMode() {
57 | return isAppendMode;
58 | }
59 |
60 | public void setAppendMode(boolean isAppendMode) {
61 | this.isAppendMode = isAppendMode;
62 | }
63 |
64 | /**
65 | * Indicates whether this TagGroup is set up to APPEND mode or DISPLAY mode. Default is false.
66 | */
67 | private boolean isAppendMode;
68 |
69 | public boolean isSelectMode() {
70 | return isSelectMode;
71 | }
72 |
73 | public void setSelectMode(boolean isSelectMode) {
74 | this.isSelectMode = isSelectMode;
75 | }
76 |
77 | private boolean isSelectMode;
78 |
79 | /**
80 | * The text to be displayed when the text of the INPUT state tag is empty.
81 | */
82 | private CharSequence mInputTagHint;
83 |
84 | /**
85 | * The bright color of the tag.
86 | */
87 | private int mBrightColor;
88 |
89 | /**
90 | * The dim color of the tag.
91 | */
92 | private int mDimColor;
93 |
94 | /**
95 | * The tag outline border stroke width, default is 0.5dp.
96 | */
97 | private float mBorderStrokeWidth;
98 |
99 | /**
100 | * The tag text size, default is 13sp.
101 | */
102 | private float mTextSize;
103 |
104 | /**
105 | * The horizontal tag spacing, default is 8.0dp.
106 | */
107 | private int mHorizontalSpacing;
108 |
109 | /**
110 | * The vertical tag spacing, default is 4.0dp.
111 | */
112 | private int mVerticalSpacing;
113 |
114 | /**
115 | * The horizontal tag padding, default is 12.0dp.
116 | */
117 | private int mHorizontalPadding;
118 |
119 | /**
120 | * The vertical tag padding, default is 3.0dp.
121 | */
122 | private int mVerticalPadding;
123 |
124 | /**
125 | * Listener used to dispatch tag change event.
126 | */
127 | private OnTagChangeListener mOnTagChangeListener;
128 |
129 | public TagGroup(Context context) {
130 | this(context, null);
131 | }
132 |
133 | public TagGroup(Context context, AttributeSet attrs) {
134 | this(context, attrs, R.attr.tagGroupStyle);
135 | }
136 |
137 | public TagGroup(Context context, AttributeSet attrs, int defStyleAttr) {
138 | super(context, attrs, defStyleAttr);
139 |
140 | default_border_stroke_width = dp2px(0.5f);
141 | default_text_size = sp2px(13.0f);
142 | default_horizontal_spacing = dp2px(8.0f);
143 | default_vertical_spacing = dp2px(4.0f);
144 | default_horizontal_padding = dp2px(12.0f);
145 | default_vertical_padding = dp2px(3.0f);
146 |
147 | // Load styled attributes.
148 | final TypedArray a = context.obtainStyledAttributes(attrs,
149 | R.styleable.TagGroup, defStyleAttr, R.style.TagGroup);
150 | try {
151 | isAppendMode = a.getBoolean(R.styleable.TagGroup_isAppendMode, false);
152 | mInputTagHint = a.getText(R.styleable.TagGroup_inputTagHint);
153 | mBrightColor = a.getColor(R.styleable.TagGroup_brightColor, default_bright_color);
154 | mDimColor = a.getColor(R.styleable.TagGroup_dimColor, default_dim_color);
155 | mBorderStrokeWidth = a.getDimension(R.styleable.TagGroup_borderStrokeWidth, default_border_stroke_width);
156 | mTextSize = a.getDimension(R.styleable.TagGroup_textSize, default_text_size);
157 | mHorizontalSpacing = (int) a.getDimension(R.styleable.TagGroup_horizontalSpacing,
158 | default_horizontal_spacing);
159 | mVerticalSpacing = (int) a.getDimension(R.styleable.TagGroup_verticalSpacing,
160 | default_vertical_spacing);
161 | mHorizontalPadding = (int) a.getDimension(R.styleable.TagGroup_horizontalPadding,
162 | default_horizontal_padding);
163 | mVerticalPadding = (int) a.getDimension(R.styleable.TagGroup_verticalPadding,
164 | default_vertical_padding);
165 | } finally {
166 | a.recycle();
167 | }
168 |
169 | setUpTagGroup();
170 | }
171 |
172 | protected void setUpTagGroup() {
173 | if (isAppendMode) {
174 | // Append the initial INPUT state tag.
175 | appendInputTag();
176 |
177 | // Set the TagGroup click listener to handle the end-input click.
178 | setOnClickListener(new OnClickListener() {
179 | @Override
180 | public void onClick(View v) {
181 | submitTag();
182 | }
183 | });
184 | }
185 | }
186 |
187 | /**
188 | * Call this to submit the INPUT state tag.
189 | */
190 | public void submitTag() {
191 | final TagView inputTag = getInputTagView();
192 | if (inputTag != null && inputTag.isInputAvailable()) {
193 | inputTag.endInput();
194 |
195 | if (mOnTagChangeListener != null) {
196 | mOnTagChangeListener.onAppend(TagGroup.this, inputTag.getText().toString());
197 | }
198 | appendInputTag(); // Append a new INPUT state tag.
199 | }
200 | }
201 |
202 | public int getBrightColor() {
203 | return mBrightColor;
204 | }
205 |
206 | public void setBrightColor(int brightColor) {
207 | mBrightColor = brightColor;
208 | invalidateAllTagsPaint();
209 | invalidate();
210 | }
211 |
212 | public int getDimColor() {
213 | return mDimColor;
214 | }
215 |
216 | public void setDimColor(int dimColor) {
217 | mDimColor = dimColor;
218 | invalidateAllTagsPaint();
219 | invalidate();
220 | }
221 |
222 | public float getBorderStrokeWidth() {
223 | return mBorderStrokeWidth;
224 | }
225 |
226 | public void setBorderStrokeWidth(float borderStrokeWidth) {
227 | mBorderStrokeWidth = borderStrokeWidth;
228 | invalidateAllTagsPaint();
229 | requestLayout();
230 | }
231 |
232 | public float getTextSize() {
233 | return mTextSize;
234 | }
235 |
236 | public void setTextSize(float textSize) {
237 | mTextSize = textSize;
238 | invalidateAllTagsPaint();
239 | requestLayout();
240 | }
241 |
242 | public int getHorizontalSpacing() {
243 | return mHorizontalSpacing;
244 | }
245 |
246 | public void setHorizontalSpacing(int horizontalSpacing) {
247 | mHorizontalSpacing = horizontalSpacing;
248 | requestLayout();
249 | }
250 |
251 | public int getVerticalSpacing() {
252 | return mVerticalSpacing;
253 | }
254 |
255 | public void setVerticalSpacing(int verticalSpacing) {
256 | mVerticalSpacing = verticalSpacing;
257 | requestLayout();
258 | }
259 |
260 | public int getHorizontalPadding() {
261 | return mHorizontalPadding;
262 | }
263 |
264 | public void setHorizontalPadding(int horizontalPadding) {
265 | mHorizontalPadding = horizontalPadding;
266 | requestLayout();
267 | }
268 |
269 | public int getVerticalPadding() {
270 | return mVerticalPadding;
271 | }
272 |
273 | public void setVerticalPadding(int verticalPadding) {
274 | mVerticalPadding = verticalPadding;
275 | requestLayout();
276 | }
277 |
278 | /**
279 | * Invalidate all tag views' paint in this group.
280 | */
281 | protected void invalidateAllTagsPaint() {
282 | final int count = getChildCount();
283 | for (int i = 0; i < count; i++) {
284 | TagView tagView = getTagViewAt(i);
285 | tagView.invalidatePaint();
286 | }
287 | }
288 |
289 | @Override
290 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
291 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
292 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
293 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
294 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
295 |
296 | measureChildren(widthMeasureSpec, heightMeasureSpec);
297 |
298 | int width = 0;
299 | int height = 0;
300 |
301 | int row = 0; // The row counter.
302 | int rowWidth = 0; // Calc the current row width.
303 | int rowMaxHeight = 0; // Calc the max tag height, in current row.
304 |
305 | final int count = getChildCount();
306 | for (int i = 0; i < count; i++) {
307 | final View child = getChildAt(i);
308 | final int childWidth = child.getMeasuredWidth();
309 | final int childHeight = child.getMeasuredHeight();
310 |
311 | if (child.getVisibility() != GONE) {
312 | rowWidth += childWidth;
313 | if (rowWidth > widthSize) { // Next line.
314 | rowWidth = childWidth; // The next row width.
315 | height += rowMaxHeight + mVerticalSpacing;
316 | rowMaxHeight = childHeight; // The next row max height.
317 | row++;
318 | } else { // This line.
319 | rowMaxHeight = Math.max(rowMaxHeight, childHeight);
320 | }
321 | rowWidth += mHorizontalSpacing;
322 | }
323 | }
324 | // Account for the last row height.
325 | height += rowMaxHeight;
326 |
327 | // Account for the padding too.
328 | height += getPaddingTop() + getPaddingBottom();
329 |
330 | // If the tags grouped in one row, set the width to wrap the tags.
331 | if (row == 0) {
332 | width = rowWidth;
333 | width += getPaddingLeft() + getPaddingRight();
334 | } else {// If the tags grouped exceed one line, set the width to match the parent.
335 | width = widthSize;
336 | }
337 |
338 | setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
339 | heightMode == MeasureSpec.EXACTLY ? heightSize : height);
340 | }
341 |
342 | @Override
343 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
344 | final int parentLeft = getPaddingLeft();
345 | final int parentRight = r - l - getPaddingRight();
346 | final int parentTop = getPaddingTop();
347 | final int parentBottom = b - t - getPaddingBottom();
348 |
349 | int childLeft = parentLeft;
350 | int childTop = parentTop;
351 |
352 | int rowMaxHeight = 0;
353 |
354 | final int count = getChildCount();
355 | for (int i = 0; i < count; i++) {
356 | final View child = getChildAt(i);
357 | final int width = child.getMeasuredWidth();
358 | final int height = child.getMeasuredHeight();
359 |
360 | if (child.getVisibility() != GONE) {
361 | if (childLeft + width > parentRight) { // Next line
362 | childLeft = parentLeft;
363 | childTop += rowMaxHeight + mVerticalSpacing;
364 | rowMaxHeight = height;
365 | } else {
366 | rowMaxHeight = Math.max(rowMaxHeight, height);
367 | }
368 | child.layout(childLeft, childTop, childLeft + width, childTop + height);
369 |
370 | childLeft += width + mHorizontalSpacing;
371 | }
372 | }
373 | }
374 |
375 | // @Override
376 | // public Parcelable onSaveInstanceState() {
377 | // Parcelable superState = super.onSaveInstanceState();
378 | // SavedState ss = new SavedState(superState);
379 | // ss.tags = getTags();
380 | // ss.checkedPosition = getCheckedTagIndex();
381 | // if (getInputTagView() != null) {
382 | // ss.input = getInputTagView().getText().toString();
383 | // }
384 | // return ss;
385 | // }
386 |
387 | // @Override
388 | // public void onRestoreInstanceState(Parcelable state) {
389 | // if (!(state instanceof SavedState)) {
390 | // super.onRestoreInstanceState(state);
391 | // return;
392 | // }
393 | //
394 | // SavedState ss = (SavedState) state;
395 | // super.onRestoreInstanceState(ss.getSuperState());
396 | //
397 | // setTags(ss.tags);
398 | // TagView checkedTagView = getTagViewAt(ss.checkedPosition);
399 | // if (checkedTagView != null) {
400 | // checkedTagView.setChecked(true);
401 | // }
402 | // if (getInputTagView() != null) {
403 | // getInputTagView().setText(ss.input);
404 | // }
405 | // }
406 |
407 | /**
408 | * Returns the INPUT state tag view in this group.
409 | *
410 | * @return the INPUT state tag view or null if not exists
411 | */
412 | protected TagView getInputTagView() {
413 | if (isAppendMode) {
414 | final int inputTagIndex = getChildCount() - 1;
415 | final TagView inputTag = getTagViewAt(inputTagIndex);
416 | if (inputTag != null && inputTag.mState == TagView.STATE_INPUT) {
417 | return inputTag;
418 | } else {
419 | return null;
420 | }
421 | } else {
422 | return null;
423 | }
424 | }
425 |
426 | /**
427 | * Returns the INPUT state tag in this group.
428 | *
429 | * @return the INPUT state tag view or null if not exists
430 | */
431 | public String getInputTag() {
432 | final TagView inputTagView = getInputTagView();
433 | if (inputTagView != null) {
434 | return inputTagView.getText().toString();
435 | }
436 | return null;
437 | }
438 |
439 | /**
440 | * Return the last NORMAL state tag view in this group.
441 | *
442 | * @return the last NORMAL state tag view or null if not exists
443 | */
444 | protected TagView getLastNormalTagView() {
445 | final int lastNormalTagIndex = isAppendMode ? getChildCount() - 2 : getChildCount() - 1;
446 | TagView lastNormalTagView = getTagViewAt(lastNormalTagIndex);
447 | return lastNormalTagView;
448 | }
449 |
450 | /**
451 | * Returns the NORMAL state tags array in group.
452 | *
453 | * @return the tag array
454 | */
455 | public ITag[] getTags() {
456 | final int count = getChildCount();
457 | final List tagList = new ArrayList<>();
458 | for (int i = 0; i < count; i++) {
459 | final TagView tagView = getTagViewAt(i);
460 | if (tagView.mState == TagView.STATE_NORMAL) {
461 | tagList.add(tagView.getTag());
462 | }
463 | }
464 |
465 | return tagList.toArray(new ITag[]{});
466 | }
467 |
468 | /**
469 | * Returns the tag view at the specified position in the group.
470 | *
471 | * @param index the position at which to get the tag view from
472 | * @return the tag view at the specified position or null if the position
473 | * does not exists within this group
474 | */
475 | protected TagView getTagViewAt(int index) {
476 | return (TagView) getChildAt(index);
477 | }
478 |
479 | /**
480 | * Returns the checked tag view in the group.
481 | *
482 | * @return the checked tag view or null if it does not exists within this group
483 | */
484 | public TagView getCheckedTagView() {
485 | final int checkedTagIndex = getCheckedTagIndex();
486 | if (checkedTagIndex != -1) { // exists
487 | return getTagViewAt(checkedTagIndex);
488 | }
489 | return null;
490 | }
491 |
492 | /**
493 | * Return the checked tag index.
494 | *
495 | * @return the checked tag index, or -1 if there is no checked tag exists
496 | */
497 | protected int getCheckedTagIndex() {
498 | final int count = getChildCount();
499 | for (int i = 0; i < count; i++) {
500 | final TagView tagView = getTagViewAt(i);
501 | if (tagView.isChecked) {
502 | return i;
503 | }
504 | }
505 | return -1;
506 | }
507 |
508 | /**
509 | * Register a callback to be invoked when this tag group is changed.
510 | *
511 | * @param l the callback that will run
512 | */
513 | public void setOnTagChangeListener(OnTagChangeListener l) {
514 | mOnTagChangeListener = l;
515 | }
516 |
517 |
518 | /**
519 | *
520 | */
521 | protected void appendInputTag() {
522 | appendInputTag(null);
523 | }
524 |
525 | /**
526 | * Append a INPUT state tag to this group. It will check the group state first.
527 | *
528 | * @param tag the tag text
529 | */
530 | protected void appendInputTag(ITag tag) {
531 | TagView lastTag = getInputTagView();
532 | if (lastTag != null) {
533 | throw new IllegalStateException("Already has a INPUT state tag in group. " +
534 | "You must call endInput() before you append new one.");
535 | }
536 |
537 | TagView tagView = new TagView(getContext(), TagView.STATE_INPUT, tag);
538 | tagView.setOnClickListener(new OnTagClickListener());
539 | addView(tagView);
540 | }
541 |
542 |
543 | public void setTagsList(List tagList) {
544 | setTags(tagList.toArray(new ITag[]{}));
545 | }
546 |
547 | /**
548 | * Set the NORMAL state tag to this group. It will remove all tags first.
549 | *
550 | * @param tags the tag list to set
551 | */
552 | public void setTags(ITag... tags) {
553 | removeAllViews();
554 | for (final ITag tag : tags) {
555 | appendTag(tag);
556 | }
557 |
558 | if (isAppendMode) {
559 | appendInputTag();
560 | }
561 | }
562 |
563 | /**
564 | * Append NORMAL state tag to this group.
565 | *
566 | * @param tag the tag to append
567 | */
568 | protected void appendTag(ITag tag) {
569 | if (isAppendMode||isSelectMode) {
570 | final int appendIndex = getChildCount();
571 | final TagView tagView = new TagView(getContext(), TagView.STATE_NORMAL, tag);
572 | tagView.setOnClickListener(new OnTagClickListener());
573 | addView(tagView, appendIndex);
574 | } else {
575 | final TagView tagView = new TagView(getContext(), TagView.STATE_NORMAL, tag);
576 | addView(tagView);
577 | }
578 | }
579 |
580 | public float dp2px(float dp) {
581 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
582 | getResources().getDisplayMetrics());
583 | }
584 |
585 | public float sp2px(float sp) {
586 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
587 | getResources().getDisplayMetrics());
588 | }
589 |
590 | @Override
591 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
592 | return new LayoutParams(getContext(), attrs);
593 | }
594 |
595 | /**
596 | * Interface definition for a callback to be invoked when a tag group is changed.
597 | */
598 | public interface OnTagChangeListener {
599 | /**
600 | * Called when a tag has been appended to the group.
601 | *
602 | * @param tag the appended tag
603 | */
604 | void onAppend(TagGroup tagGroup, String tag);
605 |
606 | /**
607 | * Called when a tag has been deleted from the the group.
608 | *
609 | * @param tag the deleted tag.
610 | */
611 | void onDelete(TagGroup tagGroup, String tag);
612 | }
613 |
614 | /**
615 | * Per-child layout information for layouts.
616 | */
617 | public static class LayoutParams extends ViewGroup.LayoutParams {
618 | public LayoutParams(Context c, AttributeSet attrs) {
619 | super(c, attrs);
620 | }
621 |
622 | public LayoutParams(int width, int height) {
623 | super(width, height);
624 | }
625 |
626 | public LayoutParams(ViewGroup.LayoutParams source) {
627 | super(source);
628 | }
629 | }
630 |
631 | /**
632 | * For {@link com.paychat.material.TagGroup} save and restore state.
633 | */
634 | // static class SavedState extends BaseSavedState {
635 | // int tagCount;
636 | // ITag[] tags;
637 | // int checkedPosition;
638 | // String input;
639 | //
640 | // public SavedState(Parcel source) {
641 | // super(source);
642 | // tagCount = source.readInt();
643 | // tags = new ITag[tagCount];
644 | // tags= source.readParcelableArray();
645 | // //source.readStringArray(tags);
646 | // checkedPosition = source.readInt();
647 | // input = source.readString();
648 | // }
649 | //
650 | // public SavedState(Parcelable superState) {
651 | // super(superState);
652 | // }
653 | //
654 | // @Override
655 | // public void writeToParcel(Parcel dest, int flags) {
656 | // super.writeToParcel(dest, flags);
657 | // tagCount = tags.length;
658 | // dest.writeInt(tagCount);
659 | // dest.writeParcelableArray(tags);
660 | // dest.writeInt(checkedPosition);
661 | // dest.writeString(input);
662 | // }
663 | //
664 | // public static final Creator CREATOR =
665 | // new Creator() {
666 | // public SavedState createFromParcel(Parcel in) {
667 | // return new SavedState(in);
668 | // }
669 | //
670 | // public SavedState[] newArray(int size) {
671 | // return new SavedState[size];
672 | // }
673 | // };
674 | // }
675 |
676 | /**
677 | * The tag view click listener.
678 | */
679 | class OnTagClickListener implements OnClickListener {
680 | @Override
681 | public void onClick(View v) {
682 | final TagView clickedTagView = (TagView) v;
683 | if (clickedTagView.mState == TagView.STATE_INPUT) {
684 | // If the clicked tag is in INPUT state,
685 | // uncheck the previous checked tag if exists.
686 | final TagView checkedTagView = getCheckedTagView();
687 | if (checkedTagView != null) {
688 | checkedTagView.setChecked(false);
689 | }
690 | } else {
691 | // If the clicked tag is checked, remove the tag
692 | // and dispatch the tag group changed event.
693 | if (clickedTagView.isChecked&&isAppendMode) {
694 | removeView(clickedTagView);
695 | if (mOnTagChangeListener != null) {
696 | //if(isAppendMode)
697 | mOnTagChangeListener.onDelete(TagGroup.this, clickedTagView.getText().toString());
698 | }
699 | } else {
700 | // If the clicked tag is unchecked, uncheck the previous checked
701 | // tag if exists, then set the clicked tag checked.
702 | final TagView checkedTagView = getCheckedTagView();
703 | if (checkedTagView != null) {
704 | checkedTagView.setChecked(false);
705 | }else{
706 | clickedTagView.setChecked(true);
707 | }
708 |
709 | }
710 | }
711 | }
712 | }
713 |
714 | /**
715 | * The tag view which has two states can be either NORMAL or INPUT.
716 | */
717 | public class TagView extends TextView {
718 | public static final int STATE_NORMAL = 1;
719 | public static final int STATE_INPUT = 2;
720 |
721 | /**
722 | * The current state.
723 | */
724 | private int mState;
725 |
726 | /**
727 | * Indicates the tag if checked.
728 | */
729 | private boolean isChecked = false;
730 |
731 | /**
732 | * The paint of tag outline border and text.
733 | */
734 | private Paint mPaint;
735 |
736 | /**
737 | * The paint of the checked mark.
738 | */
739 | private Paint mMarkPaint;
740 |
741 | /**
742 | * The rect for the tag's left corner drawing.
743 | */
744 | private RectF mLeftCornerRectF;
745 |
746 | /**
747 | * The rect for the tag's right corner drawing.
748 | */
749 | private RectF mRightCornerRectF;
750 |
751 | /**
752 | * The rect for the tag's horizontal blank fill area.
753 | */
754 | private RectF mHorizontalBlankFillRectF;
755 |
756 | /**
757 | * The rect for the tag's vertical blank fill area.
758 | */
759 | private RectF mVerticalBlankFillRectF;
760 |
761 | /**
762 | * The rect for the checked mark draw bound.
763 | */
764 | private RectF mCheckedMarkDrawBound;
765 |
766 | /**
767 | * The offset to the text.
768 | */
769 | private int mCheckedMarkOffset;
770 |
771 | /**
772 | * The path for draw the tag's outline border.
773 | */
774 | private Path mBorderPath;
775 |
776 | /**
777 | * The path effect provide draw the dash border.
778 | */
779 | private PathEffect mPathEffect;
780 |
781 | public ITag getTag() {
782 | return iTag;
783 | }
784 |
785 | ITag iTag;
786 | public TagView(Context context, int state, ITag text) {
787 | super(context);
788 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
789 | mMarkPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
790 | mMarkPaint.setColor(Color.WHITE);
791 | mMarkPaint.setStrokeWidth(4);
792 | iTag=text;
793 | mLeftCornerRectF = new RectF();
794 | mRightCornerRectF = new RectF();
795 |
796 | mHorizontalBlankFillRectF = new RectF();
797 | mVerticalBlankFillRectF = new RectF();
798 |
799 | mCheckedMarkDrawBound = new RectF();
800 | mCheckedMarkOffset = 3;
801 |
802 | mBorderPath = new Path();
803 | mPathEffect = new DashPathEffect(new float[]{10, 5}, 0);
804 |
805 | setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding);
806 | setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
807 | LayoutParams.WRAP_CONTENT));
808 |
809 | setGravity(Gravity.CENTER);
810 | if(text!=null)
811 | setText(text.getTag());
812 | setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
813 |
814 | mState = state;
815 |
816 | setClickable(isAppendMode||isSelectMode);
817 | setFocusable(state == STATE_INPUT);
818 | setFocusableInTouchMode(state == STATE_INPUT);
819 | setHint(state == STATE_INPUT ? mInputTagHint : null);
820 | setMovementMethod(state == STATE_INPUT ? ArrowKeyMovementMethod.getInstance() : null);
821 |
822 | if (state == STATE_INPUT) {
823 | requestFocus();
824 |
825 | // Handle the ENTER key down.
826 | setOnEditorActionListener(new OnEditorActionListener() {
827 | @Override
828 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
829 | if (actionId == EditorInfo.IME_NULL
830 | && (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
831 | && event.getAction() == KeyEvent.ACTION_DOWN)) {
832 | if (isInputAvailable()) {
833 | // If the input content is available, end the input and dispatch
834 | // the event, then append a new INPUT state tag.
835 | endInput();
836 | if (mOnTagChangeListener != null) {
837 | mOnTagChangeListener.onAppend(TagGroup.this, getText().toString());
838 | }
839 | appendInputTag();
840 | }
841 | return true;
842 | }
843 | return false;
844 | }
845 | });
846 |
847 | // Handle the BACKSPACE key down.
848 | setOnKeyListener(new OnKeyListener() {
849 | @Override
850 | public boolean onKey(View v, int keyCode, KeyEvent event) {
851 | if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
852 | // If the input content is empty, check or remove the last NORMAL state tag.
853 | if (TextUtils.isEmpty(getText().toString())) {
854 | TagView lastNormalTagView = getLastNormalTagView();
855 | if (lastNormalTagView != null) {
856 | if (lastNormalTagView.isChecked) {
857 | removeView(lastNormalTagView);
858 | if (mOnTagChangeListener != null) {
859 | if (isAppendMode)
860 | mOnTagChangeListener.onDelete(TagGroup.this, lastNormalTagView.getText().toString());
861 | }
862 | } else {
863 | final TagView checkedTagView = getCheckedTagView();
864 | if (checkedTagView != null) {
865 | checkedTagView.setChecked(false);
866 | }
867 | lastNormalTagView.setChecked(true);
868 | }
869 | return true;
870 | }
871 | }
872 | }
873 | return false;
874 | }
875 | });
876 |
877 | // Handle the INPUT tag content changed.
878 | addTextChangedListener(new TextWatcher() {
879 | @Override
880 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
881 | // When the INPUT state tag changed, uncheck the checked tag if exists.
882 | final TagView checkedTagView = getCheckedTagView();
883 | if (checkedTagView != null) {
884 | checkedTagView.setChecked(false);
885 | }
886 | }
887 |
888 | @Override
889 | public void onTextChanged(CharSequence s, int start, int before, int count) {
890 | }
891 |
892 | @Override
893 | public void afterTextChanged(Editable s) {
894 | }
895 | });
896 | }
897 |
898 | invalidatePaint();
899 | }
900 |
901 | /**
902 | * Set whether this tag view is in the checked state.
903 | *
904 | * @param checked true is checked, false otherwise
905 | */
906 | public void setChecked(boolean checked) {
907 | isChecked = checked;
908 | // Make the checked mark drawing region.
909 | if (isAppendMode)
910 | setPadding(mHorizontalPadding,
911 | mVerticalPadding,
912 | isChecked ? (int) (mHorizontalPadding + getHeight() / 2.5f + mCheckedMarkOffset)
913 | : mHorizontalPadding,
914 | mVerticalPadding);
915 | invalidatePaint();
916 | }
917 |
918 | /**
919 | * Call this method to end this tag's INPUT state.
920 | */
921 | public void endInput() {
922 | // Make the view not focusable.
923 | setFocusable(false);
924 | setFocusableInTouchMode(false);
925 | // Set the hint empty, make the TextView measure correctly.
926 | setHint(null);
927 | // Take away the cursor.
928 | setMovementMethod(null);
929 |
930 | mState = STATE_NORMAL;
931 | invalidatePaint();
932 | requestLayout();
933 | }
934 |
935 | @Override
936 | protected boolean getDefaultEditable() {
937 | return true;
938 | }
939 |
940 | /**
941 | * Indicates whether the input content is available.
942 | *
943 | * @return True if the input content is available, false otherwise.
944 | */
945 | public boolean isInputAvailable() {
946 | return getText() != null && getText().length() > 0;
947 | }
948 |
949 | private void invalidatePaint() {
950 | if (mState == STATE_NORMAL) {
951 | if (isChecked) {
952 | mPaint.setStyle(Paint.Style.FILL);
953 | mPaint.setColor(mBrightColor);
954 | mPaint.setPathEffect(null);
955 | setTextColor(Color.WHITE);
956 | } else {
957 | mPaint.setStyle(Paint.Style.STROKE);
958 | mPaint.setStrokeWidth(mBorderStrokeWidth);
959 | mPaint.setColor(mBrightColor);
960 | mPaint.setPathEffect(null);
961 | setTextColor(mBrightColor);
962 | }
963 |
964 | } else if (mState == STATE_INPUT) {
965 | mPaint.setStyle(Paint.Style.STROKE);
966 | mPaint.setStrokeWidth(mBorderStrokeWidth);
967 | mPaint.setColor(mDimColor);
968 | mPaint.setPathEffect(mPathEffect);
969 | setTextColor(mDimColor);
970 | }
971 | }
972 |
973 | @Override
974 | protected void onDraw(Canvas canvas) {
975 | if (isChecked) {
976 | canvas.drawArc(mLeftCornerRectF, -180, 90, true, mPaint);
977 | canvas.drawArc(mLeftCornerRectF, -270, 90, true, mPaint);
978 | canvas.drawArc(mRightCornerRectF, -90, 90, true, mPaint);
979 | canvas.drawArc(mRightCornerRectF, 0, 90, true, mPaint);
980 | canvas.drawRect(mHorizontalBlankFillRectF, mPaint);
981 | canvas.drawRect(mVerticalBlankFillRectF, mPaint);
982 |
983 |
984 | if (isAppendMode)
985 | { canvas.save();
986 |
987 | canvas.rotate(45, mCheckedMarkDrawBound.centerX(), mCheckedMarkDrawBound.centerY());
988 | canvas.drawLine(mCheckedMarkDrawBound.left, mCheckedMarkDrawBound.centerY(),
989 | mCheckedMarkDrawBound.right, mCheckedMarkDrawBound.centerY(), mMarkPaint);
990 | canvas.drawLine(mCheckedMarkDrawBound.centerX(), mCheckedMarkDrawBound.top,
991 | mCheckedMarkDrawBound.centerX(), mCheckedMarkDrawBound.bottom, mMarkPaint);
992 | canvas.restore();
993 | }
994 |
995 | } else {
996 | canvas.drawPath(mBorderPath, mPaint);
997 | }
998 | super.onDraw(canvas);
999 | }
1000 |
1001 | @Override
1002 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1003 | super.onSizeChanged(w, h, oldw, oldh);
1004 | // Cast to int
1005 | int left = (int) mBorderStrokeWidth;
1006 | int top = (int) mBorderStrokeWidth;
1007 | int right = (int) (left + w - mBorderStrokeWidth * 2);
1008 | int bottom = (int) (top + h - mBorderStrokeWidth * 2);
1009 |
1010 | int d = bottom - top;
1011 |
1012 | mLeftCornerRectF.set(left, top, left + d, top + d);
1013 | mRightCornerRectF.set(right - d, top, right, top + d);
1014 |
1015 | mBorderPath.reset();
1016 | mBorderPath.addArc(mLeftCornerRectF, -180, 90);
1017 | mBorderPath.addArc(mLeftCornerRectF, -270, 90);
1018 | mBorderPath.addArc(mRightCornerRectF, -90, 90);
1019 | mBorderPath.addArc(mRightCornerRectF, 0, 90);
1020 |
1021 | int l = (int) (d / 2.0f);
1022 | mBorderPath.moveTo(left + l, top);
1023 | mBorderPath.lineTo(right - l, top);
1024 |
1025 | mBorderPath.moveTo(left + l, bottom);
1026 | mBorderPath.lineTo(right - l, bottom);
1027 |
1028 | mBorderPath.moveTo(left, top + l);
1029 | mBorderPath.lineTo(left, bottom - l);
1030 |
1031 | mBorderPath.moveTo(right, top + l);
1032 | mBorderPath.lineTo(right, bottom - l);
1033 |
1034 | mHorizontalBlankFillRectF.set(left, top + l, right, bottom - l);
1035 | mVerticalBlankFillRectF.set(left + l, top, right - l, bottom);
1036 |
1037 | int m = (int) (h / 2.5f);
1038 | h = bottom - top;
1039 | mCheckedMarkDrawBound.set(right - m - mHorizontalPadding + mCheckedMarkOffset,
1040 | top + h / 2 - m / 2,
1041 | right - mHorizontalPadding + mCheckedMarkOffset,
1042 | bottom - h / 2 + m / 2);
1043 |
1044 | // Ensure the checked mark drawing region is correct across screen orientation changes.
1045 | if (isChecked) {
1046 | setPadding(mHorizontalPadding,
1047 | mVerticalPadding,
1048 | isChecked ? (int) (mHorizontalPadding + h / 2.5f + mCheckedMarkOffset)
1049 | : mHorizontalPadding,
1050 | mVerticalPadding);
1051 | }
1052 | }
1053 | }
1054 | }
1055 |
--------------------------------------------------------------------------------