├── .gitignore ├── README.MD ├── TagsGroup.iml ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tag │ │ └── tagsgroup │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tag │ │ └── tagsgroup │ │ ├── MainActivity.java │ │ ├── TagsGroup.java │ │ └── TestModel.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attr.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image └── Screenshot_2016-03-26-17-29-43.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 在[AndroidTagGroup](https://github.com/2dxgujun/AndroidTagGroup)的基础上做了些修改和拓展,以及修复了一些bug。 2 |
使用Rxjava和Lambda写了一个简单的例子(Android N已经支持Lambda,可去掉Retrolambda) 3 |
主要修改如下: 4 |
1:选中效果 5 |
2:标签文字长度限制、数量限制 6 |
3:边框的绘制作了修改 7 |
4:滑动导致长按bug 8 |
5:其它一些细节修改 9 |
10 |
11 |
效果如下图 12 |
![image](https://github.com/lin18/TagsGroup/blob/master/image/Screenshot_2016-03-26-17-29-43.png?raw=true) 13 | -------------------------------------------------------------------------------- /TagsGroup.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 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 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'me.tatarka.retrolambda' 3 | 4 | android { 5 | compileSdkVersion 23 6 | buildToolsVersion "23.0.2" 7 | 8 | defaultConfig { 9 | applicationId "com.tag.tagsgroup" 10 | minSdkVersion 14 11 | targetSdkVersion 23 12 | versionCode 1 13 | versionName "1.0" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | 26 | retrolambda { 27 | javaVersion JavaVersion.VERSION_1_7 28 | } 29 | } 30 | 31 | dependencies { 32 | compile fileTree(dir: 'libs', include: ['*.jar']) 33 | compile 'com.android.support:appcompat-v7:23.1.1' 34 | compile 'io.reactivex:rxandroid:1.1.0' 35 | compile 'io.reactivex:rxjava:1.1.0' 36 | } 37 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\soft\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tag/tagsgroup/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.tag.tagsgroup; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/tag/tagsgroup/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tag.tagsgroup; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.text.TextUtils; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | import android.view.View; 9 | import android.widget.Toast; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import rx.Observable; 14 | import rx.android.schedulers.AndroidSchedulers; 15 | 16 | public class MainActivity extends AppCompatActivity { 17 | 18 | private final static TestModel[] initAdd = new TestModel[]{new TestModel(0, "标签1"), new TestModel(1, "标签2")}; 19 | private final static TestModel[] initHot = new TestModel[]{new TestModel(0, "标签1"), new TestModel(1, "标签2"), new TestModel(2, "测试"), 20 | new TestModel(3, "微信"), new TestModel(4, "qq"), new TestModel(5, "ggg")}; 21 | private final static TestModel[] initAll = new TestModel[]{new TestModel(0, "标签1"), new TestModel(1, "标签2"), new TestModel(2, "测试"), 22 | new TestModel(3, "微信"), new TestModel(4, "qq"), new TestModel(5, "淘宝"), new TestModel(6, "UC"), new TestModel(7, "10086"), 23 | new TestModel(8, "支付宝"), new TestModel(9, "tag"), new TestModel(10, "热门"), new TestModel(11, "lbe"), new TestModel(12, "ggg")}; 24 | 25 | private TagsGroup add; 26 | private TagsGroup hot; 27 | private TagsGroup search; 28 | 29 | private String searchText; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_main); 35 | add = (TagsGroup) findViewById(R.id.add); 36 | hot = (TagsGroup) findViewById(R.id.hot); 37 | search = (TagsGroup) findViewById(R.id.search); 38 | 39 | add.setTags(initAdd); 40 | hot.setTags(initHot); 41 | hot.setSelectedTag(true, initAdd); 42 | 43 | add.setOnTagTextChangedListener(s -> { 44 | if (TextUtils.isEmpty(s)) { 45 | hot.setVisibility(View.VISIBLE); 46 | search.setVisibility(View.GONE); 47 | search.clearTag(); 48 | } else { 49 | if (!s.equals(searchText)) { 50 | hot.setVisibility(View.GONE); 51 | search.setVisibility(View.VISIBLE); 52 | Observable.timer(600, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) 53 | .flatMap(aLong -> Observable.from(initAll)) 54 | .filter(testModel -> testModel.getTag().contains(searchText)) 55 | .subscribe(testModel1 -> search.appendTag(testModel1), Throwable::printStackTrace); 56 | } 57 | } 58 | searchText = s; 59 | }); 60 | add.setOnTagChangeListener(new TagsGroup.OnTagChangeListener() { 61 | @Override 62 | public void onAppend(TagsGroup tagGroup, String tag) { 63 | showHot(tag, true); 64 | } 65 | 66 | @Override 67 | public void onDelete(TagsGroup tagGroup, Object objTag, String tag) { 68 | showHot(tag, false); 69 | } 70 | }); 71 | hot.setOnTagClickListener((viewGroup, v, objTag, tag, isSelected) -> { 72 | boolean canAdd = canAdd(); 73 | if (!isSelected) { 74 | if (canAdd) { 75 | v.setSelected(true); 76 | add.addTag(tag, objTag, false); 77 | } else { 78 | Toast.makeText(this, R.string.add_tag_limit_hint, Toast.LENGTH_SHORT).show(); 79 | } 80 | } else { 81 | add.removeTag(tag); 82 | } 83 | }); 84 | search.setOnTagClickListener((viewGroup, v, objTag, tag, isSelected) -> { 85 | if (canAdd()) { 86 | if (add.addTag(tag, true)) { 87 | 88 | } 89 | search.clearTag(); 90 | showHot(tag, true); 91 | } else { 92 | Toast.makeText(this, R.string.add_tag_limit_hint, Toast.LENGTH_SHORT).show(); 93 | } 94 | }); 95 | } 96 | 97 | private boolean canAdd() { 98 | return add.canAdd(); 99 | } 100 | 101 | private void showHot(String tag, boolean selected) { 102 | searchText = ""; 103 | hot.setVisibility(View.VISIBLE); 104 | search.setVisibility(View.GONE); 105 | hot.setSelectedTag(tag, selected); 106 | } 107 | 108 | private void search(String searchText) { 109 | 110 | } 111 | 112 | @Override 113 | public boolean onCreateOptionsMenu(Menu menu) { 114 | // Inflate the menu; this adds items to the action bar if it is present. 115 | getMenuInflater().inflate(R.menu.menu_main, menu); 116 | return true; 117 | } 118 | 119 | @Override 120 | public boolean onOptionsItemSelected(MenuItem item) { 121 | // Handle action bar item clicks here. The action bar will 122 | // automatically handle clicks on the Home/Up button, so long 123 | // as you specify a parent activity in AndroidManifest.xml. 124 | int id = item.getItemId(); 125 | 126 | //noinspection SimplifiableIfStatement 127 | if (id == R.id.action_settings) { 128 | return true; 129 | } 130 | 131 | return super.onOptionsItemSelected(item); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/tag/tagsgroup/TagsGroup.java: -------------------------------------------------------------------------------- 1 | package com.tag.tagsgroup; 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.Rect; 12 | import android.graphics.RectF; 13 | import android.os.Parcel; 14 | import android.os.Parcelable; 15 | import android.os.SystemClock; 16 | import android.text.Editable; 17 | import android.text.Selection; 18 | import android.text.TextUtils; 19 | import android.text.TextWatcher; 20 | import android.text.method.ArrowKeyMovementMethod; 21 | import android.util.AttributeSet; 22 | import android.util.TypedValue; 23 | import android.view.Gravity; 24 | import android.view.KeyEvent; 25 | import android.view.MotionEvent; 26 | import android.view.View; 27 | import android.view.ViewGroup; 28 | import android.view.inputmethod.EditorInfo; 29 | import android.view.inputmethod.InputConnection; 30 | import android.view.inputmethod.InputConnectionWrapper; 31 | import android.widget.TextView; 32 | import android.widget.Toast; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | /** 38 | * A 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 tagList = new ArrayList<>(); 411 | for (int i = 0; i < count; i++) { 412 | final TagView tagView = getTagAt(i); 413 | if (tagView.mState == TagView.STATE_NORMAL) { 414 | tagList.add(tagView.getText().toString()); 415 | } 416 | } 417 | 418 | return tagList.toArray(new String[tagList.size()]); 419 | } 420 | 421 | public void setTags(String... tags) { 422 | removeAllViews(); 423 | for (final String tag : tags) { 424 | if (isAppendMode && getTagCount() > maxSize) { 425 | Toast.makeText(getContext(), hint, Toast.LENGTH_SHORT).show(); 426 | break; 427 | } 428 | appendTag(tag); 429 | } 430 | 431 | if (isAppendMode) { 432 | appendInputTag(); 433 | } 434 | } 435 | 436 | public void setTags(List tagList) { 437 | removeAllViews(); 438 | for (final T tag : tagList) { 439 | if (isAppendMode && getTagCount() > maxSize) { 440 | Toast.makeText(getContext(), hint, Toast.LENGTH_SHORT).show(); 441 | break; 442 | } 443 | appendTag(tag); 444 | } 445 | 446 | if (isAppendMode) { 447 | // appendInputTag(); 448 | } 449 | } 450 | 451 | /** 452 | * Set the tags. It will remove all previous tags first. 453 | * 454 | * @param tags the tag list to set. 455 | */ 456 | public void setTags(T... tags) { 457 | removeAllViews(); 458 | for (final T tag : tags) { 459 | if (isAppendMode && getTagCount() > maxSize) { 460 | Toast.makeText(getContext(), hint, Toast.LENGTH_SHORT).show(); 461 | break; 462 | } 463 | appendTag(tag); 464 | } 465 | 466 | if (isAppendMode) { 467 | // appendInputTag(); 468 | } 469 | } 470 | 471 | public void setSelectedTag(boolean selected, T... tags) { 472 | if (tags == null || tags.length == 0) return; 473 | for (final T tag : tags) { 474 | setSelectedTag(tag.getTag(), selected); 475 | } 476 | } 477 | 478 | public boolean addTag(CharSequence s) { 479 | return addTag(s, false); 480 | } 481 | 482 | public boolean addTag(CharSequence s, boolean isSelected) { 483 | return addTag(s, -1, isSelected); 484 | } 485 | 486 | public boolean addTag(CharSequence s, Object objTag, boolean isSelected) { 487 | if (isAppendMode && getTagCount() > maxSize) { 488 | Toast.makeText(getContext(), hint, Toast.LENGTH_SHORT).show(); 489 | return false; 490 | } 491 | if (!TextUtils.isEmpty(s)) { 492 | if (isAppendMode) { 493 | final TagView inputTag = getInputTag(); 494 | if (inputTag != null) { 495 | removeView(inputTag); 496 | } 497 | } 498 | boolean isExist = false; 499 | final int count = getChildCount(); 500 | for (int i = 0; i < count; i++) { 501 | final TagView tag = getTagAt(i); 502 | if (s.equals(tag.getText().toString())) { 503 | isExist = true; 504 | break; 505 | } 506 | } 507 | if (!isExist) { 508 | appendTag(s, objTag, isSelected); 509 | } 510 | if (isAppendMode) { 511 | appendInputTag(); 512 | } 513 | return !isExist; 514 | } 515 | return false; 516 | } 517 | 518 | public void removeTag(String s) { 519 | if (!TextUtils.isEmpty(s)) { 520 | final int count = getChildCount(); 521 | for (int i = 0; i < count; i++) { 522 | final TagView tag = getTagAt(i); 523 | if (s.equals(tag.getText().toString())) { 524 | deleteTag(tag); 525 | return; 526 | } 527 | } 528 | } 529 | } 530 | 531 | public void clearTag() { 532 | removeAllViews(); 533 | if (isAppendMode) { 534 | appendInputTag(); 535 | } 536 | } 537 | 538 | public void setSelectedTag(String s, boolean selected) { 539 | if (!TextUtils.isEmpty(s)) { 540 | final int count = getChildCount(); 541 | for (int i = 0; i < count; i++) { 542 | final TagView tag = getTagAt(i); 543 | if (s.equals(tag.getText().toString())) { 544 | tag.setSelected(selected); 545 | return; 546 | } 547 | } 548 | } 549 | } 550 | 551 | /** 552 | * Returns the tag view at the specified position in the group. 553 | * 554 | * @param index the position at which to get the tag view from. 555 | * @return the tag view at the specified position or null if the position 556 | * does not exists within this group. 557 | */ 558 | protected TagView getTagAt(int index) { 559 | return (TagView) getChildAt(index); 560 | } 561 | 562 | /** 563 | * Returns the checked tag view in the group. 564 | * 565 | * @return the checked tag view or null if not exists. 566 | */ 567 | protected TagView getCheckedTag() { 568 | final int checkedTagIndex = getCheckedTagIndex(); 569 | if (checkedTagIndex != -1) { 570 | return getTagAt(checkedTagIndex); 571 | } 572 | return null; 573 | } 574 | 575 | /** 576 | * Return the checked tag index. 577 | * 578 | * @return the checked tag index, or -1 if not exists. 579 | */ 580 | protected int getCheckedTagIndex() { 581 | final int count = getChildCount(); 582 | for (int i = 0; i < count; i++) { 583 | final TagView tag = getTagAt(i); 584 | if (tag.isChecked) { 585 | return i; 586 | } 587 | } 588 | return -1; 589 | } 590 | 591 | public int getTagCount() { 592 | return getChildCount(); 593 | } 594 | 595 | public boolean canAdd() { 596 | return getChildCount()-1 < maxSize; 597 | } 598 | 599 | /** 600 | * Register a callback to be invoked when this tag group is changed. 601 | * 602 | * @param l the callback that will run 603 | */ 604 | public void setOnTagChangeListener(OnTagChangeListener l) { 605 | mOnTagChangeListener = l; 606 | } 607 | 608 | /** 609 | * @see #appendInputTag(String) 610 | */ 611 | protected void appendInputTag() { 612 | appendInputTag(null); 613 | } 614 | 615 | /** 616 | * Append a INPUT tag to this group. It will throw an exception if there has a previous INPUT tag. 617 | * 618 | * @param tag the tag text. 619 | */ 620 | protected void appendInputTag(String tag) { 621 | final TagView previousInputTag = getInputTag(); 622 | if (previousInputTag != null) { 623 | throw new IllegalStateException("Already has a INPUT tag in group."); 624 | } 625 | 626 | final TagView newInputTag = new TagView(getContext(), TagView.STATE_INPUT, tag); 627 | newInputTag.setOnClickListener(mInternalTagClickListener); 628 | addView(newInputTag); 629 | } 630 | 631 | /** 632 | * Append tag to this group. 633 | * 634 | * @param tag the tag to append. 635 | */ 636 | protected void appendTag(CharSequence tag) { 637 | addTag(tag, false); 638 | } 639 | 640 | protected void appendTag(T tag) { 641 | addTag(tag.getTag(), tag, false); 642 | } 643 | 644 | protected void appendTag(CharSequence tag, boolean isSelected) { 645 | appendTag(tag, -1, isSelected); 646 | } 647 | 648 | protected void appendTag(CharSequence tag, Object objTag, boolean isSelected) { 649 | final TagView newTag = new TagView(getContext(), TagView.STATE_NORMAL, objTag, isSelected, tag); 650 | newTag.setOnClickListener(mInternalTagClickListener); 651 | addView(newTag); 652 | } 653 | 654 | public float dp2px(float dp) { 655 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, 656 | getResources().getDisplayMetrics()); 657 | } 658 | 659 | public float sp2px(float sp) { 660 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, 661 | getResources().getDisplayMetrics()); 662 | } 663 | 664 | @Override 665 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 666 | return new TagsGroup.LayoutParams(getContext(), attrs); 667 | } 668 | 669 | /** 670 | * Register a callback to be invoked when a tag is clicked. 671 | * 672 | * @param l the callback that will run. 673 | */ 674 | public void setOnTagClickListener(OnTagClickListener l) { 675 | mOnTagClickListener = l; 676 | } 677 | public void setOnTagTextChangedListener(OnTagTextChangedListener l) { 678 | mOnTagTextChangedListener = l; 679 | } 680 | 681 | protected void deleteTag(TagView tagView) { 682 | removeView(tagView); 683 | if (mOnTagChangeListener != null) { 684 | mOnTagChangeListener.onDelete(TagsGroup.this, tagView.getTag(), tagView.getText().toString()); 685 | } 686 | } 687 | 688 | /** 689 | * Interface definition for a callback to be invoked when a tag group is changed. 690 | */ 691 | public interface OnTagChangeListener { 692 | /** 693 | * Called when a tag has been appended to the group. 694 | * 695 | * @param tag the appended tag. 696 | */ 697 | void onAppend(TagsGroup tagGroup, String tag); 698 | 699 | /** 700 | * Called when a tag has been deleted from the the group. 701 | * 702 | * @param tag the deleted tag. 703 | */ 704 | void onDelete(TagsGroup tagGroup, Object objTag, String tag); 705 | } 706 | 707 | /** 708 | * Interface definition for a callback to be invoked when a tag is clicked. 709 | */ 710 | public interface OnTagClickListener { 711 | /** 712 | * Called when a tag has been clicked. 713 | * 714 | * @param tag The tag text of the tag that was clicked. 715 | */ 716 | void onTagClick(View viewGroup, View v, Object objTag, String tag, boolean isSelected); 717 | } 718 | 719 | public interface OnTagTextChangedListener { 720 | void onTextChanged(String s); 721 | } 722 | /** 723 | * Per-child layout information for layouts. 724 | */ 725 | public static class LayoutParams extends ViewGroup.LayoutParams { 726 | public LayoutParams(Context c, AttributeSet attrs) { 727 | super(c, attrs); 728 | } 729 | 730 | public LayoutParams(int width, int height) { 731 | super(width, height); 732 | } 733 | } 734 | 735 | /** 736 | * For {@link TagsGroup} save and restore state. 737 | */ 738 | static class SavedState extends BaseSavedState { 739 | public static final Parcelable.Creator CREATOR = 740 | new Parcelable.Creator() { 741 | public SavedState createFromParcel(Parcel in) { 742 | return new SavedState(in); 743 | } 744 | 745 | public SavedState[] newArray(int size) { 746 | return new SavedState[size]; 747 | } 748 | }; 749 | int tagCount; 750 | String[] tags; 751 | int checkedPosition; 752 | String input; 753 | 754 | public SavedState(Parcel source) { 755 | super(source); 756 | tagCount = source.readInt(); 757 | tags = new String[tagCount]; 758 | source.readStringArray(tags); 759 | checkedPosition = source.readInt(); 760 | input = source.readString(); 761 | } 762 | 763 | public SavedState(Parcelable superState) { 764 | super(superState); 765 | } 766 | 767 | @Override 768 | public void writeToParcel(Parcel dest, int flags) { 769 | super.writeToParcel(dest, flags); 770 | tagCount = tags.length; 771 | dest.writeInt(tagCount); 772 | dest.writeStringArray(tags); 773 | dest.writeInt(checkedPosition); 774 | dest.writeString(input); 775 | } 776 | } 777 | 778 | /** 779 | * The tag view click listener for internal use. 780 | */ 781 | class InternalTagClickListener implements OnClickListener { 782 | @Override 783 | public void onClick(View v) { 784 | final TagView tag = (TagView) v; 785 | if (isAppendMode) { 786 | if (tag.mState == TagView.STATE_INPUT) { 787 | // If the clicked tag is in INPUT state, uncheck the previous checked tag if exists. 788 | final TagView checkedTag = getCheckedTag(); 789 | if (checkedTag != null) { 790 | checkedTag.setChecked(false); 791 | } 792 | } else { 793 | // If the clicked tag is currently checked, delete the tag. 794 | // if (tag.isChecked) { 795 | final TagView checkedTag = getCheckedTag(); 796 | if (checkedTag != null) { 797 | checkedTag.setChecked(false); 798 | } 799 | deleteTag(tag); 800 | // } else { 801 | // // If the clicked tag is unchecked, uncheck the previous checked tag if exists, 802 | // // then check the clicked tag. 803 | // final TagView checkedTag = getCheckedTag(); 804 | // if (checkedTag != null) { 805 | // checkedTag.setChecked(false); 806 | // } 807 | // tag.setChecked(true); 808 | // } 809 | } 810 | } else { 811 | // tag.setSelected(!tag.isSelected); 812 | if (mOnTagClickListener != null) { 813 | mOnTagClickListener.onTagClick(TagsGroup.this, tag, tag.getTag(), tag.getText().toString(), tag.isSelected); 814 | } 815 | } 816 | } 817 | } 818 | 819 | /** 820 | * The tag view which has two states can be either NORMAL or INPUT. 821 | */ 822 | class TagView extends TextView { 823 | public static final int STATE_NORMAL = 1; 824 | public static final int STATE_INPUT = 2; 825 | 826 | /** The offset to the text. */ 827 | private static final int CHECKED_MARKER_OFFSET = 3; 828 | 829 | /** The stroke width of the checked marker */ 830 | private static final int CHECKED_MARKER_STROKE_WIDTH = 4; 831 | 832 | /** The current state. */ 833 | private int mState; 834 | 835 | /** Indicates the tag if checked. */ 836 | private boolean isChecked = false; 837 | 838 | private boolean isSelected = false; 839 | 840 | /** Indicates the tag if pressed. */ 841 | private boolean isPressed = false; 842 | 843 | private Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 844 | 845 | private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 846 | 847 | private Paint mCheckedMarkerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 848 | 849 | private RectF rectF = new RectF(); 850 | /** The rect for the tag's left corner drawing. */ 851 | private RectF mLeftCornerRectF = new RectF(); 852 | 853 | /** The rect for the tag's right corner drawing. */ 854 | private RectF mRightCornerRectF = new RectF(); 855 | 856 | /** The rect for the tag's horizontal blank fill area. */ 857 | private RectF mHorizontalBlankFillRectF = new RectF(); 858 | 859 | /** The rect for the tag's vertical blank fill area. */ 860 | private RectF mVerticalBlankFillRectF = new RectF(); 861 | 862 | /** The rect for the checked mark draw bound. */ 863 | private RectF mCheckedMarkerBound = new RectF(); 864 | 865 | /** Used to detect the touch event. */ 866 | private Rect mOutRect = new Rect(); 867 | 868 | /** The path for draw the tag's outline border. */ 869 | private Path mBorderPath = new Path(); 870 | 871 | /** The path effect provide draw the dash border. */ 872 | private PathEffect mPathEffect = new DashPathEffect(new float[]{10, 5}, 0); 873 | 874 | { 875 | mBorderPaint.setStyle(Paint.Style.STROKE); 876 | mBorderPaint.setStrokeWidth(borderStrokeWidth); 877 | mBackgroundPaint.setStyle(Paint.Style.FILL); 878 | mCheckedMarkerPaint.setStyle(Paint.Style.FILL); 879 | mCheckedMarkerPaint.setStrokeWidth(CHECKED_MARKER_STROKE_WIDTH); 880 | mCheckedMarkerPaint.setColor(checkedMarkerColor); 881 | } 882 | 883 | public TagView(Context context, final int state, CharSequence text) { 884 | this(context, state, false, text); 885 | } 886 | 887 | public TagView(Context context, final int state, boolean isSelected, CharSequence text) { 888 | this(context, state, -1, isSelected, text); 889 | } 890 | 891 | public TagView(Context context, final int state, Object objTag, boolean isSelected, CharSequence text) { 892 | super(context); 893 | setTag(objTag); 894 | this.isSelected = isSelected; 895 | setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); 896 | setLayoutParams(new TagsGroup.LayoutParams( 897 | TagsGroup.LayoutParams.WRAP_CONTENT, 898 | TagsGroup.LayoutParams.WRAP_CONTENT)); 899 | 900 | setGravity(Gravity.CENTER); 901 | // setImeOptions(EditorInfo.IME_ACTION_DONE); 902 | setText(text); 903 | setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 904 | 905 | mState = state; 906 | 907 | setClickable(isAppendMode); 908 | setFocusable(state == STATE_INPUT); 909 | setFocusableInTouchMode(state == STATE_INPUT); 910 | setHint(state == STATE_INPUT ? inputHint : null); 911 | setMovementMethod(state == STATE_INPUT ? ArrowKeyMovementMethod.getInstance() : null); 912 | 913 | // Interrupted long click event to avoid PAUSE popup. 914 | setOnLongClickListener(new OnLongClickListener() { 915 | @Override 916 | public boolean onLongClick(View v) { 917 | return state != STATE_INPUT; 918 | } 919 | }); 920 | 921 | if (state == STATE_INPUT) { 922 | requestFocus(); 923 | 924 | // Handle the ENTER key down. 925 | setOnEditorActionListener(new OnEditorActionListener() { 926 | @Override 927 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 928 | if (actionId == EditorInfo.IME_NULL 929 | && (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER 930 | && event.getAction() == KeyEvent.ACTION_DOWN)) { 931 | appendInput(); 932 | return true; 933 | } 934 | return false; 935 | } 936 | }); 937 | 938 | // Handle the BACKSPACE key down. 939 | setOnKeyListener(new OnKeyListener() { 940 | @Override 941 | public boolean onKey(View v, int keyCode, KeyEvent event) { 942 | if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) { 943 | // If the input content is empty, check or remove the last NORMAL state tag. 944 | if (TextUtils.isEmpty(getText().toString())) { 945 | TagView lastNormalTagView = getLastNormalTagView(); 946 | if (lastNormalTagView != null) { 947 | if (lastNormalTagView.isChecked) { 948 | deleteTag(lastNormalTagView); 949 | } else { 950 | final TagView checkedTagView = getCheckedTag(); 951 | if (checkedTagView != null) { 952 | checkedTagView.setChecked(false); 953 | } 954 | lastNormalTagView.setChecked(true); 955 | } 956 | return true; 957 | } 958 | } 959 | } 960 | return false; 961 | } 962 | }); 963 | 964 | // Handle the INPUT tag content changed. 965 | addTextChangedListener(new TextWatcher() { 966 | @Override 967 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 968 | // When the INPUT state tag changed, uncheck the checked tag if exists. 969 | final TagView checkedTagView = getCheckedTag(); 970 | if (checkedTagView != null) { 971 | checkedTagView.setChecked(false); 972 | } 973 | } 974 | 975 | @Override 976 | public void onTextChanged(CharSequence s, int start, int before, int count) { 977 | if (mState == STATE_INPUT) { 978 | String str = s.toString().trim(); 979 | if (tagMaxLength > 0 && str.length() > tagMaxLength) { 980 | setText(str.substring(0, tagMaxLength)); 981 | Selection.setSelection((Editable) getText(), tagMaxLength); 982 | } else { 983 | if (mOnTagTextChangedListener != null) { 984 | mOnTagTextChangedListener.onTextChanged(str); 985 | } 986 | } 987 | } 988 | } 989 | 990 | @Override 991 | public void afterTextChanged(Editable s) { 992 | } 993 | }); 994 | } 995 | 996 | invalidatePaint(); 997 | } 998 | 999 | private void appendInput() { 1000 | if (getTagCount() > maxSize) { 1001 | Toast.makeText(getContext(), hint, Toast.LENGTH_SHORT).show(); 1002 | } else { 1003 | if (isInputAvailable()) { 1004 | // If the input content is available, end the input and dispatch 1005 | // the event, then append a new INPUT state tag. 1006 | if (endInput()) { 1007 | if (mOnTagChangeListener != null) { 1008 | mOnTagChangeListener.onAppend(TagsGroup.this, getText().toString().trim()); 1009 | } 1010 | appendInputTag(); 1011 | } 1012 | } 1013 | } 1014 | } 1015 | 1016 | private void simulateClick() { 1017 | final long downTime = SystemClock.uptimeMillis(); 1018 | final MotionEvent downEvent = MotionEvent.obtain( 1019 | downTime, downTime, MotionEvent.ACTION_DOWN, getWidth(), getHeight(), 0); 1020 | final MotionEvent upEvent = MotionEvent.obtain( 1021 | downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, getWidth(), getHeight(), 0); 1022 | super.onTouchEvent(downEvent); 1023 | super.onTouchEvent(upEvent); 1024 | 1025 | downEvent.recycle(); 1026 | upEvent.recycle(); 1027 | } 1028 | 1029 | /** 1030 | * Set whether this tag view is in the checked state. 1031 | * 1032 | * @param checked true is checked, false otherwise 1033 | */ 1034 | public void setChecked(boolean checked) { 1035 | isChecked = checked; 1036 | // Make the checked mark drawing region. 1037 | setPadding(horizontalPadding, 1038 | verticalPadding, 1039 | /*isChecked ? (int) (horizontalPadding + getHeight() / 2.5f + CHECKED_MARKER_OFFSET) 1040 | : */horizontalPadding, 1041 | verticalPadding); 1042 | invalidatePaint(); 1043 | } 1044 | 1045 | public void setSelected(boolean selected) { 1046 | isSelected = selected; 1047 | invalidatePaint(); 1048 | } 1049 | 1050 | /** 1051 | * Call this method to end this tag's INPUT state. 1052 | */ 1053 | public boolean endInput() { 1054 | if (isInputAvailable()) { 1055 | String str = getText().toString().trim(); 1056 | for (int i = 0; i < getChildCount()-1; i++) { 1057 | final TagView tag = getTagAt(i); 1058 | if (str.equals(tag.getText().toString())) { 1059 | setText(null); 1060 | return false; 1061 | } 1062 | } 1063 | } 1064 | // Make the view not focusable. 1065 | setFocusable(false); 1066 | setFocusableInTouchMode(false); 1067 | // Set the hint empty, make the TextView measure correctly. 1068 | setHint(null); 1069 | // Take away the cursor. 1070 | setMovementMethod(null); 1071 | 1072 | mState = STATE_NORMAL; 1073 | if (isInputAvailable()) { 1074 | setText(getText().toString().trim()); 1075 | } 1076 | invalidatePaint(); 1077 | requestLayout(); 1078 | return true; 1079 | } 1080 | 1081 | @Override 1082 | protected boolean getDefaultEditable() { 1083 | return true; 1084 | } 1085 | 1086 | /** 1087 | * Indicates whether the input content is available. 1088 | * 1089 | * @return True if the input content is available, false otherwise. 1090 | */ 1091 | public boolean isInputAvailable() { 1092 | return getText() != null && getText().toString().trim().length() > 0; 1093 | } 1094 | 1095 | private void invalidatePaint() { 1096 | if (isAppendMode) { 1097 | if (mState == STATE_INPUT) { 1098 | mBorderPaint.setColor(dashBorderColor); 1099 | mBorderPaint.setPathEffect(mPathEffect); 1100 | mBackgroundPaint.setColor(backgroundColor); 1101 | setHintTextColor(inputHintColor); 1102 | setTextColor(inputTextColor); 1103 | } else { 1104 | mBorderPaint.setPathEffect(null); 1105 | if (isChecked) { 1106 | mBorderPaint.setColor(checkedBorderColor); 1107 | mBackgroundPaint.setColor(checkedBackgroundColor); 1108 | setTextColor(checkedTextColor); 1109 | } else { 1110 | mBorderPaint.setColor(selectedBorderColor); 1111 | mBackgroundPaint.setColor(selectedBackgroundColor); 1112 | setTextColor(selectedTextColor); 1113 | } 1114 | } 1115 | } else { 1116 | if (isSelected) { 1117 | mBorderPaint.setColor(selectedBorderColor); 1118 | mBackgroundPaint.setColor(selectedBackgroundColor); 1119 | setTextColor(selectedTextColor); 1120 | } else { 1121 | mBorderPaint.setColor(borderColor); 1122 | mBackgroundPaint.setColor(backgroundColor); 1123 | setTextColor(textColor); 1124 | } 1125 | } 1126 | 1127 | if (isPressed) { 1128 | mBackgroundPaint.setColor(pressedBackgroundColor); 1129 | } 1130 | } 1131 | 1132 | @Override 1133 | protected void onDraw(Canvas canvas) { 1134 | // canvas.drawArc(mLeftCornerRectF, -180, 90, true, mBackgroundPaint); 1135 | // canvas.drawArc(mLeftCornerRectF, -270, 90, true, mBackgroundPaint); 1136 | // canvas.drawArc(mRightCornerRectF, -90, 90, true, mBackgroundPaint); 1137 | // canvas.drawArc(mRightCornerRectF, 0, 90, true, mBackgroundPaint); 1138 | // canvas.drawRect(mHorizontalBlankFillRectF, mBackgroundPaint); 1139 | // canvas.drawRect(mVerticalBlankFillRectF, mBackgroundPaint); 1140 | if (cornerRadius < 0) { 1141 | cornerRadius = getHeight() / 2; 1142 | } 1143 | canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, mBackgroundPaint); 1144 | 1145 | /*if (isChecked) { 1146 | canvas.save(); 1147 | canvas.rotate(45, mCheckedMarkerBound.centerX(), mCheckedMarkerBound.centerY()); 1148 | canvas.drawLine(mCheckedMarkerBound.left, mCheckedMarkerBound.centerY(), 1149 | mCheckedMarkerBound.right, mCheckedMarkerBound.centerY(), mCheckedMarkerPaint); 1150 | canvas.drawLine(mCheckedMarkerBound.centerX(), mCheckedMarkerBound.top, 1151 | mCheckedMarkerBound.centerX(), mCheckedMarkerBound.bottom, mCheckedMarkerPaint); 1152 | canvas.restore(); 1153 | }*/ 1154 | // canvas.drawPath(mBorderPath, mBorderPaint); 1155 | canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, mBorderPaint); 1156 | super.onDraw(canvas); 1157 | } 1158 | 1159 | @Override 1160 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1161 | super.onSizeChanged(w, h, oldw, oldh); 1162 | int left = (int) borderStrokeWidth; 1163 | int top = (int) borderStrokeWidth; 1164 | int right = (int) (left + w - borderStrokeWidth * 2); 1165 | int bottom = (int) (top + h - borderStrokeWidth * 2); 1166 | 1167 | int d = bottom - top; 1168 | 1169 | // mLeftCornerRectF.set(left, top, left + d, top + d); 1170 | // mRightCornerRectF.set(right - d, top, right, top + d); 1171 | // 1172 | // mBorderPath.reset(); 1173 | // mBorderPath.addArc(mLeftCornerRectF, -180, 90); 1174 | // mBorderPath.addArc(mLeftCornerRectF, -270, 90); 1175 | // mBorderPath.addArc(mRightCornerRectF, -90, 90); 1176 | // mBorderPath.addArc(mRightCornerRectF, 0, 90); 1177 | // 1178 | // int l = (int) (d / 2.0f); 1179 | // mBorderPath.moveTo(left + l, top); 1180 | // mBorderPath.lineTo(right - l, top); 1181 | // 1182 | // mBorderPath.moveTo(left + l, bottom); 1183 | // mBorderPath.lineTo(right - l, bottom); 1184 | // 1185 | // mBorderPath.moveTo(left, top + l); 1186 | // mBorderPath.lineTo(left, bottom - l); 1187 | // 1188 | // mBorderPath.moveTo(right, top + l); 1189 | // mBorderPath.lineTo(right, bottom - l); 1190 | // 1191 | // mHorizontalBlankFillRectF.set(left, top + l, right, bottom - l); 1192 | // mVerticalBlankFillRectF.set(left + l, top, right - l, bottom); 1193 | rectF.set(0 + borderStrokeWidth, 0 + borderStrokeWidth, w - borderStrokeWidth * 2, h - borderStrokeWidth * 2); 1194 | 1195 | int m = (int) (h / 2.5f); 1196 | h = bottom - top; 1197 | mCheckedMarkerBound.set(right - m - horizontalPadding + CHECKED_MARKER_OFFSET, 1198 | top + h / 2 - m / 2, 1199 | right - horizontalPadding + CHECKED_MARKER_OFFSET, 1200 | bottom - h / 2 + m / 2); 1201 | 1202 | // Ensure the checked mark drawing region is correct across screen orientation changes. 1203 | if (isChecked) { 1204 | setPadding(horizontalPadding, 1205 | verticalPadding, 1206 | (int) (horizontalPadding + h / 2.5f + CHECKED_MARKER_OFFSET), 1207 | verticalPadding); 1208 | } 1209 | } 1210 | 1211 | @Override 1212 | public boolean dispatchTouchEvent(MotionEvent event) { 1213 | return super.dispatchTouchEvent(event); 1214 | } 1215 | 1216 | @Override 1217 | public boolean onTouchEvent(MotionEvent event) { 1218 | if (mState == STATE_INPUT) { 1219 | // The INPUT tag doesn't change background color on the touch event. 1220 | return super.onTouchEvent(event); 1221 | } 1222 | 1223 | switch (event.getAction()) { 1224 | case MotionEvent.ACTION_DOWN: { 1225 | getDrawingRect(mOutRect); 1226 | isPressed = true; 1227 | invalidatePaint(); 1228 | invalidate(); 1229 | break; 1230 | } 1231 | case MotionEvent.ACTION_MOVE: { 1232 | if (!mOutRect.contains((int) event.getX(), (int) event.getY())) { 1233 | isPressed = false; 1234 | invalidatePaint(); 1235 | invalidate(); 1236 | } 1237 | break; 1238 | } 1239 | case MotionEvent.ACTION_CANCEL: 1240 | case MotionEvent.ACTION_UP: { 1241 | isPressed = false; 1242 | invalidatePaint(); 1243 | invalidate(); 1244 | break; 1245 | } 1246 | } 1247 | return super.onTouchEvent(event); 1248 | } 1249 | 1250 | @Override 1251 | public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 1252 | return new ZanyInputConnection(super.onCreateInputConnection(outAttrs), true); 1253 | } 1254 | 1255 | /** 1256 | * Solve edit text delete(backspace) key detect, see 1257 | * Android: Backspace in WebView/BaseInputConnection 1258 | */ 1259 | private class ZanyInputConnection extends InputConnectionWrapper { 1260 | public ZanyInputConnection(android.view.inputmethod.InputConnection target, boolean mutable) { 1261 | super(target, mutable); 1262 | } 1263 | 1264 | @Override 1265 | public boolean deleteSurroundingText(int beforeLength, int afterLength) { 1266 | // magic: in latest Android, deleteSurroundingText(1, 0) will be called for backspace 1267 | if (beforeLength == 1 && afterLength == 0) { 1268 | // backspace 1269 | return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) 1270 | && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); 1271 | } 1272 | return super.deleteSurroundingText(beforeLength, afterLength); 1273 | } 1274 | } 1275 | } 1276 | 1277 | public interface TagModel { 1278 | String getTag(); 1279 | } 1280 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tag/tagsgroup/TestModel.java: -------------------------------------------------------------------------------- 1 | package com.tag.tagsgroup; 2 | 3 | /** 4 | * 5 | */ 6 | public class TestModel implements TagsGroup.TagModel { 7 | 8 | private int id; 9 | private String name; 10 | 11 | public TestModel(int id, String name) { 12 | this.id = id; 13 | this.name = name; 14 | } 15 | 16 | public int getId() { 17 | return id; 18 | } 19 | 20 | public void setId(int id) { 21 | this.id = id; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setName(String name) { 29 | this.name = name; 30 | } 31 | 32 | @Override 33 | public String getTag() { 34 | return name; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 23 | 24 | 28 | 29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lin18/TagsGroup/01fc228a5a709c1325420e8b282ae42083f9c9e2/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lin18/TagsGroup/01fc228a5a709c1325420e8b282ae42083f9c9e2/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lin18/TagsGroup/01fc228a5a709c1325420e8b282ae42083f9c9e2/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lin18/TagsGroup/01fc228a5a709c1325420e8b282ae42083f9c9e2/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TagsGroup 3 | 4 | Hello world! 5 | Settings 6 | 添加标签 7 | 最多只能添加5个标签 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 36 | 37 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.5.0' 9 | classpath 'me.tatarka:gradle-retrolambda:3.2.5' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lin18/TagsGroup/01fc228a5a709c1325420e8b282ae42083f9c9e2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Mar 26 13:13:24 CST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /image/Screenshot_2016-03-26-17-29-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lin18/TagsGroup/01fc228a5a709c1325420e8b282ae42083f9c9e2/image/Screenshot_2016-03-26-17-29-43.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------