├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── .DS_Store │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── attrs.xml │ │ │ │ └── styles.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ └── layout │ │ │ │ └── activity_main.xml │ │ ├── java │ │ │ └── blue │ │ │ │ └── stack │ │ │ │ └── grouptag │ │ │ │ ├── lib │ │ │ │ ├── ITag.java │ │ │ │ └── TagGroup.java │ │ │ │ ├── MyTag.java │ │ │ │ └── MainActivity.java │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── blue │ │ └── stack │ │ └── grouptag │ │ └── ApplicationTest.java ├── build.gradle ├── proguard-rules.pro └── app.iml ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── GroupTag.iml ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /app/src/main/res/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranavlathigara/GroupTag/HEAD/app/src/main/res/.DS_Store -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranavlathigara/GroupTag/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranavlathigara/GroupTag/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranavlathigara/GroupTag/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranavlathigara/GroupTag/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranavlathigara/GroupTag/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GroupTag 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 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.2.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/blue/stack/grouptag/lib/ITag.java: -------------------------------------------------------------------------------- 1 | package blue.stack.grouptag.lib; 2 | 3 | /** 4 | * Created by BunnyBlue on 3/8/15. 5 | */ 6 | public interface ITag { 7 | public int getID(); 8 | public void setID(int id); 9 | public String getTag(); 10 | public void setTag(String tag); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | -------------------------------------------------------------------------------- /app/src/androidTest/java/blue/stack/grouptag/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package blue.stack.grouptag; 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/java/blue/stack/grouptag/MyTag.java: -------------------------------------------------------------------------------- 1 | package blue.stack.grouptag; 2 | 3 | import blue.stack.grouptag.lib.ITag; 4 | 5 | /** 6 | * Created by BunnyBlue on 3/11/15. 7 | */ 8 | public class MyTag implements ITag { 9 | String tag; 10 | 11 | public MyTag(String tag) { 12 | this.tag = tag; 13 | } 14 | 15 | @Override 16 | 17 | public int getID() { 18 | return 0; 19 | } 20 | 21 | @Override 22 | public void setID(int id) { 23 | 24 | } 25 | 26 | @Override 27 | public String getTag() { 28 | return tag; 29 | } 30 | 31 | @Override 32 | public void setTag(String tag) { 33 | this.tag=tag; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.0" 6 | 7 | defaultConfig { 8 | applicationId "blue.stack.grouptag" 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | // compile 'com.android.support:appcompat-v7:22.+' 25 | } 26 | -------------------------------------------------------------------------------- /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 /Users/BunnyBlue/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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /GroupTag.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bunny Blue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/blue/stack/grouptag/MainActivity.java: -------------------------------------------------------------------------------- 1 | package blue.stack.grouptag; 2 | 3 | 4 | import android.app.Activity; 5 | import android.graphics.Color; 6 | import android.os.Bundle; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import blue.stack.grouptag.lib.ITag; 12 | import blue.stack.grouptag.lib.TagGroup; 13 | 14 | 15 | public class MainActivity extends Activity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | TagGroup tagGroup= (TagGroup) findViewById(R.id.tags); 22 | List tags=new ArrayList<>(); 23 | 24 | 25 | tagGroup.setAppendMode(true); 26 | tagGroup.setSelectMode(false); 27 | tagGroup.setTagsList(tags); 28 | tagGroup.setBrightColor(Color.RED); 29 | TagGroup tagGroup2= (TagGroup) findViewById(R.id.tags2); 30 | for (int i=0;i<5;i++){ 31 | tags.add(new MyTag("i"+i)); 32 | 33 | } 34 | tagGroup2.setAppendMode(false); 35 | tagGroup2.setSelectMode(true); 36 | tagGroup2.setTagsList(tags); 37 | tagGroup2.setBrightColor(Color.RED); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 21 | 22 | 29 | 30 | 38 | 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidTagGroup 2 | 3 | 4 | The AndroidTagGroup is a layout for a set of tags.You can use it to group people, books or anything you want. 5 | 6 | Also you can contribute new idea to me. 7 | 8 | 9 | # Demo 10 | 11 | ### Screenshot 12 | ![Demo screenshot](http://ww2.sinaimg.cn/large/bce2dea9gw1epouvl2mvuj20dw0ehmyb.jpg) 13 | 14 | ### Append Tag 15 | ![Append mode](http://ww4.sinaimg.cn/large/bce2dea9gw1epouw5y9ijj20dw06dt8x.jpg) 16 | 17 | ### Delete tag 18 | ![Delete tag](http://ww3.sinaimg.cn/large/bce2dea9gw1epouweadrqj20dw05dglt.jpg) 19 | 20 | 21 | 22 | # Usage 23 | 24 | 25 | 26 | ## Step 1 27 | 28 | Use it in your own code: 29 | ```xml 30 | 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 | ![Present color](http://ww4.sinaimg.cn/large/bce2dea9gw1epouwn8og4j20dw0a5aal.jpg) 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 | ![Dimension illustrate](http://ww2.sinaimg.cn/large/bce2dea9gw1epov0i8x6kj20rk054q4g.jpg) 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 | Follow me on Weibo 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 | 8 | 9 | 10 | 11 | 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 | --------------------------------------------------------------------------------