├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── chip_error.png ├── dc1.png ├── dc2.png ├── dc3.png └── dc4.png ├── library ├── .gitignore ├── android-material-chips.pom ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── doodle │ │ └── android │ │ └── chips │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── doodle │ │ │ └── android │ │ │ └── chips │ │ │ ├── ChipsView.java │ │ │ ├── model │ │ │ └── Contact.java │ │ │ ├── util │ │ │ └── Common.java │ │ │ └── views │ │ │ ├── ChipsEditText.java │ │ │ └── ChipsVerticalLinearLayout.java │ └── res │ │ ├── drawable-v21 │ │ └── btn_trans_base10.xml │ │ ├── drawable │ │ ├── btn_trans_base10.xml │ │ ├── chip_background.xml │ │ ├── circle.xml │ │ ├── ic_close_24dp.xml │ │ ├── ic_error_red_24dp.xml │ │ └── ic_person_24dp.xml │ │ ├── layout │ │ ├── chips_view.xml │ │ └── dialog_chips_email.xml │ │ ├── values-de │ │ └── localizable.xml │ │ ├── values-es │ │ └── localizable.xml │ │ ├── values-fi │ │ └── localizable.xml │ │ ├── values-fr │ │ └── localizable.xml │ │ ├── values-it │ │ └── localizable.xml │ │ ├── values-nl │ │ └── localizable.xml │ │ ├── values-pt-rBR │ │ └── localizable.xml │ │ ├── values-sv │ │ └── localizable.xml │ │ ├── values-tr │ │ └── localizable.xml │ │ ├── values-v21 │ │ └── fonts.xml │ │ └── values │ │ ├── attr.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── fonts.xml │ │ ├── localizable.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── doodle │ └── android │ └── chips │ └── ExampleUnitTest.java ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── doodle │ │ └── android │ │ └── chips │ │ └── sample │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── fonts │ │ │ ├── FiraSans-Medium.ttf │ │ │ └── FiraSans-Regular.ttf │ ├── java │ │ └── com │ │ │ └── doodle │ │ │ └── android │ │ │ └── chips │ │ │ └── sample │ │ │ ├── ChipsEmailDialogFragment.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable │ │ ├── ic_bug_report_24dp.xml │ │ └── ic_close_24dp.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── item_checkable_contact.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── doodle │ └── android │ └── chips │ └── sample │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | ----- 3 | ##### Features 4 | * Add chips to the layout live preview. 5 | * Add custom chips margin: `app:cv_chips_margin="${dimension}"`. 6 | * Add custom TypeFace: `.setTypeface(...)`. 7 | * There is no Email dialog anymore. Show your own dialog when `onInputNotRecognized` gets called. 8 | * Add option to use initials instead of the default person icon: `useInitials(...)`. 9 | * The placeholder has no alpha anymore. 10 | 11 | 1.1.0 12 | ----- 13 | 14 | ##### Features 15 | 16 | * Added attribute `app:cv_max_height="${dimension}"` to make the ChipView's content scrollable beyond this limit (fixes [\#8](https://github.com/DoodleScheduling/android-material-chips/issues/8)) 17 | * Added attribute `app:cv_vertical_spacing="${dimension}"` to allow configurable spacing between rows (fixes [\#4](https://github.com/DoodleScheduling/android-material-chips/issues/4)) 18 | 19 | ##### Misc 20 | 21 | * Added `getChips()` (fixes [\#6](https://github.com/DoodleScheduling/android-material-chips/issues/6)) 22 | * Added `getEditText()`, e.g. to allow the ChipsView to be made non-editable (fixes [\#2](https://github.com/DoodleScheduling/android-material-chips/issues/2)) 23 | * Improved the cursor's alignment to be centered towards the Chips in its row 24 | * Improved the sample application 25 | 26 | ##### Upgrade Notes 27 | 28 | As the ChipsView itself is now a ScrollView you may want to remove any ScrollView that you already used to wrap the ChipView's content and use `app:cv_max_height` instead. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016 Doodle AG 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Material Chips 2 | 3 | A different approach to implement android material chips since using transformed images inside an EditText cause too many exceptions on older devices and older versions. 4 | 5 | ## Features 6 | **Enter an email address and it will automatically transform into a chip** 7 | 8 |

9 | 10 | 11 |

12 | 13 | **Email validation dialog** 14 | 15 | *** UPDATE: The dialog is not included in version 1.2.0 anymore. Show your own dialog instead. *** 16 | 17 |

18 | 19 | 20 |

21 | 22 | **Customize your layout and text** 23 | 24 | ## Sample 25 | **APK:** [sample-apk-1.0.1](https://github.com/DoodleScheduling/android-material-chips/releases/download/1.0.1/android-material-chips-1.0.1-sample.apk) 26 | 27 | ## Download 28 | 29 | **Gradle:** 30 | 31 | via [jCenter](https://bintray.com/doodlescheduling/com.doodle/doodle-android-chips) 32 | ```gradle 33 | buildscript { 34 | repositories { 35 | jcenter() 36 | } 37 | } 38 | 39 | dependencies { 40 | compile 'com.doodle.android:android-material-chips:1.2.0' 41 | } 42 | ``` 43 | 44 | via [JitPack.io](https://jitpack.io/#DoodleScheduling/android-material-chips) 45 | ```gradle 46 | repositories { 47 | maven { url "https://jitpack.io" } 48 | } 49 | 50 | dependencies { 51 | compile 'com.github.DoodleScheduling:android-material-chips:1.2.0' 52 | } 53 | ``` 54 | 55 | ## Usage 56 | 57 | Use the ChipsView class in your layout file. 58 | 59 | ```xml 60 | 64 | ``` 65 | 66 | ### Customize 67 | 68 | **Layout** 69 | 70 | Include ```xmlns:app="http://schemas.android.com/apk/res-auto"``` and customize your layout file. 71 | 72 | ```xml 73 | 159 | 160 |

161 | 162 | ## Apps with Android chips: 163 | 164 | * [Doodle](https://doodle.com) Android App: [Play Store](https://play.google.com/store/apps/details?id=com.doodle.android) 165 | 166 | ## License 167 | 168 | Copyright (C) 2016 Doodle AG. 169 | 170 | Licensed under the Apache License, Version 2.0 (the "License"); 171 | you may not use this file except in compliance with the License. 172 | You may obtain a copy of the License at 173 | 174 | http://www.apache.org/licenses/LICENSE-2.0 175 | 176 | Unless required by applicable law or agreed to in writing, software 177 | distributed under the License is distributed on an "AS IS" BASIS, 178 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 179 | See the License for the specific language governing permissions and 180 | limitations under the License. 181 | 182 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:2.2.3' 8 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | jcenter() 15 | } 16 | } 17 | 18 | task clean(type: Delete) { 19 | delete rootProject.buildDir 20 | } 21 | -------------------------------------------------------------------------------- /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/DoodleScheduling/android-material-chips/2e14e87090e80a08ad818d176c7580fe4f186990/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Feb 27 11:21:31 CET 2017 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.14.1-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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /images/chip_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoodleScheduling/android-material-chips/2e14e87090e80a08ad818d176c7580fe4f186990/images/chip_error.png -------------------------------------------------------------------------------- /images/dc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoodleScheduling/android-material-chips/2e14e87090e80a08ad818d176c7580fe4f186990/images/dc1.png -------------------------------------------------------------------------------- /images/dc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoodleScheduling/android-material-chips/2e14e87090e80a08ad818d176c7580fe4f186990/images/dc2.png -------------------------------------------------------------------------------- /images/dc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoodleScheduling/android-material-chips/2e14e87090e80a08ad818d176c7580fe4f186990/images/dc3.png -------------------------------------------------------------------------------- /images/dc4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoodleScheduling/android-material-chips/2e14e87090e80a08ad818d176c7580fe4f186990/images/dc4.png -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/android-material-chips.pom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.doodle.android 7 | android-material-chips 8 | 1.2.0 9 | 2016 10 | aar 11 | 12 | Android Material Chips 13 | Implementation of Android Material Chips 14 | https://github.com/DoodleScheduling/android-material-chips 15 | 16 | 17 | Doodle AG 18 | http://doodle.com 19 | 20 | 21 | 22 | GitHub Issues 23 | https://github.com/DoodleScheduling/android-material-chips/issues 24 | 25 | 26 | 27 | https://github.com/DoodleScheduling/android-material-chips.git 28 | https://github.com/DoodleScheduling/android-material-chips.git 29 | https://github.com/DoodleScheduling/android-material-chips 30 | 31 | 32 | 33 | 34 | Apache 2.0 35 | http://www.apache.org/licenses/LICENSE-2.0.txt 36 | 37 | 38 | 39 | 40 | 41 | com.android.support 42 | appcompat-v7 43 | 23.1.1 44 | compile 45 | 46 | 47 | com.makeramen 48 | roundedimageview 49 | 2.0.1 50 | compile 51 | 52 | 53 | com.squareup.picasso 54 | picasso 55 | 2.5.2 56 | compile 57 | 58 | 59 | com.rengwuxian.materialedittext 60 | library 61 | 2.1.4 62 | compile 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 16 9 | targetSdkVersion 25 10 | versionCode 4 11 | versionName "1.2.0" 12 | setProperty("archivesBaseName", "android-material-chips-$versionName") 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile 'com.makeramen:roundedimageview:2.0.1' 25 | compile 'com.squareup.picasso:picasso:2.5.2' 26 | compile 'com.android.support:appcompat-v7:25.1.1' 27 | compile 'com.rengwuxian.materialedittext:library:2.1.4' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | 31 | task sourcesJar(type: Jar) { 32 | from android.sourceSets.main.java.srcDirs 33 | classifier = 'sources' 34 | } 35 | 36 | task javadoc(type: Javadoc) { 37 | source = android.sourceSets.main.java.srcDirs 38 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 39 | } 40 | 41 | task javadocJar(type: Jar, dependsOn: javadoc) { 42 | classifier = 'javadoc' 43 | from javadoc.destinationDir 44 | } 45 | 46 | artifacts { 47 | archives javadocJar 48 | archives sourcesJar 49 | } 50 | -------------------------------------------------------------------------------- /library/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/alex/Library/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 | 19 | #picasso 20 | -dontwarn com.squareup.okhttp.** 21 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/doodle/android/chips/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.doodle.android.chips; 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 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/java/com/doodle/android/chips/ChipsView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Doodle AG. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.doodle.android.chips; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.Context; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Color; 23 | import android.graphics.PorterDuff; 24 | import android.graphics.Rect; 25 | import android.graphics.Typeface; 26 | import android.graphics.drawable.GradientDrawable; 27 | import android.net.Uri; 28 | import android.os.Build; 29 | import android.support.annotation.ColorInt; 30 | import android.support.annotation.NonNull; 31 | import android.support.annotation.Nullable; 32 | import android.support.v4.content.ContextCompat; 33 | import android.text.Editable; 34 | import android.text.InputType; 35 | import android.text.Spannable; 36 | import android.text.Spanned; 37 | import android.text.TextUtils; 38 | import android.text.TextWatcher; 39 | import android.util.AttributeSet; 40 | import android.util.Log; 41 | import android.util.TypedValue; 42 | import android.view.KeyEvent; 43 | import android.view.View; 44 | import android.view.ViewGroup; 45 | import android.view.inputmethod.EditorInfo; 46 | import android.view.inputmethod.InputConnection; 47 | import android.view.inputmethod.InputConnectionWrapper; 48 | import android.widget.EditText; 49 | import android.widget.ImageView; 50 | import android.widget.LinearLayout; 51 | import android.widget.RelativeLayout; 52 | import android.widget.ScrollView; 53 | import android.widget.TextView; 54 | 55 | import com.doodle.android.chips.model.Contact; 56 | import com.doodle.android.chips.util.Common; 57 | import com.doodle.android.chips.views.ChipsEditText; 58 | import com.doodle.android.chips.views.ChipsVerticalLinearLayout; 59 | import com.squareup.picasso.Callback; 60 | import com.squareup.picasso.Picasso; 61 | 62 | import java.util.ArrayList; 63 | import java.util.Collections; 64 | import java.util.List; 65 | 66 | public class ChipsView extends ScrollView implements ChipsEditText.InputConnectionWrapperInterface { 67 | 68 | // 69 | private static final String TAG = "ChipsView"; 70 | private static final int CHIP_HEIGHT = 32; // dp 71 | private static final int SPACING_TOP = 4; // dp 72 | private static final int SPACING_BOTTOM = 4; // dp 73 | public static final int DEFAULT_VERTICAL_SPACING = 1; // dp 74 | private static final int DEFAULT_MAX_HEIGHT = -1; 75 | // 76 | 77 | // 78 | private int mChipsBgRes = R.drawable.chip_background; 79 | // 80 | 81 | // 82 | private int mMaxHeight; // px 83 | private int mVerticalSpacing; 84 | 85 | private int mChipsColor; 86 | private int mChipsColorClicked; 87 | private int mChipsColorErrorClicked; 88 | private int mChipsBgColor; 89 | private int mChipsBgColorIndelible; 90 | private int mChipsBgColorClicked; 91 | private int mChipsBgColorErrorClicked; 92 | private int mChipsTextColor; 93 | private int mChipsTextColorIndelible; 94 | private int mChipsTextColorClicked; 95 | private int mChipsTextColorErrorClicked; 96 | private int mChipsPlaceholderResId; 97 | private 98 | @ColorInt 99 | int mChipsPlaceholderTint; 100 | private int mChipsDeleteResId; 101 | private String mChipsHintText; 102 | 103 | private int mChipsMargin; 104 | // 105 | 106 | // 107 | private float mDensity; 108 | private RelativeLayout mChipsContainer; 109 | private ChipsListener mChipsListener; 110 | private ChipsEditText mEditText; 111 | private ChipsVerticalLinearLayout mRootChipsLayout; 112 | private EditTextListener mEditTextListener; 113 | private List mChipList = new ArrayList<>(); 114 | private Object mCurrentEditTextSpan; 115 | private ChipValidator mChipsValidator; 116 | private Typeface mTypeface; 117 | 118 | // initials 119 | private boolean mUseInitials = false; 120 | private int mInitialsTextSize; 121 | private Typeface mInitialsTypeface; 122 | @ColorInt 123 | private int mInitialsTextColor; 124 | // 125 | 126 | // 127 | public ChipsView(Context context) { 128 | super(context); 129 | init(); 130 | } 131 | 132 | public ChipsView(Context context, AttributeSet attrs) { 133 | super(context, attrs); 134 | initAttr(context, attrs); 135 | init(); 136 | } 137 | 138 | public ChipsView(Context context, AttributeSet attrs, int defStyleAttr) { 139 | super(context, attrs, defStyleAttr); 140 | initAttr(context, attrs); 141 | init(); 142 | } 143 | 144 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 145 | public ChipsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 146 | super(context, attrs, defStyleAttr, defStyleRes); 147 | initAttr(context, attrs); 148 | init(); 149 | } 150 | // 151 | 152 | @Override 153 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 154 | if(mMaxHeight != DEFAULT_MAX_HEIGHT) { 155 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST); 156 | } 157 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 158 | } 159 | 160 | @Override 161 | protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 162 | return true; 163 | } 164 | 165 | // 166 | private void initAttr(Context context, AttributeSet attrs) { 167 | TypedArray a = context.getTheme().obtainStyledAttributes( 168 | attrs, 169 | R.styleable.ChipsView, 170 | 0, 0); 171 | try { 172 | mMaxHeight = a.getDimensionPixelSize(R.styleable.ChipsView_cv_max_height, DEFAULT_MAX_HEIGHT); 173 | mVerticalSpacing = a.getDimensionPixelSize(R.styleable.ChipsView_cv_vertical_spacing, (int) (DEFAULT_VERTICAL_SPACING * mDensity)); 174 | mChipsColor = a.getColor(R.styleable.ChipsView_cv_color, ContextCompat.getColor(context, R.color.base30)); 175 | mChipsColorClicked = a.getColor(R.styleable.ChipsView_cv_color_clicked, ContextCompat.getColor(context, R.color.colorPrimaryDark)); 176 | mChipsColorErrorClicked = a.getColor(R.styleable.ChipsView_cv_color_error_clicked, ContextCompat.getColor(context, R.color.color_error)); 177 | mChipsBgColor = a.getColor(R.styleable.ChipsView_cv_bg_color, ContextCompat.getColor(context, R.color.base10)); 178 | mChipsBgColorClicked = a.getColor(R.styleable.ChipsView_cv_bg_color_clicked, ContextCompat.getColor(context, R.color.blue)); 179 | mChipsBgColorIndelible = a.getColor(R.styleable.ChipsView_cv_bg_color_indelible, mChipsBgColor); 180 | mChipsBgColorErrorClicked = a.getColor(R.styleable.ChipsView_cv_bg_color_clicked, ContextCompat.getColor(context, R.color.color_error)); 181 | mChipsTextColor = a.getColor(R.styleable.ChipsView_cv_text_color, Color.BLACK); 182 | mChipsTextColorClicked = a.getColor(R.styleable.ChipsView_cv_text_color_clicked, Color.WHITE); 183 | mChipsTextColorErrorClicked = a.getColor(R.styleable.ChipsView_cv_text_color_clicked, Color.WHITE); 184 | mChipsTextColorIndelible = a.getColor(R.styleable.ChipsView_cv_text_color_indelible, mChipsTextColor); 185 | mChipsPlaceholderResId = a.getResourceId(R.styleable.ChipsView_cv_icon_placeholder, R.drawable.ic_person_24dp); 186 | mChipsPlaceholderTint = a.getColor(R.styleable.ChipsView_cv_icon_placeholder_tint, 0); 187 | mChipsDeleteResId = a.getResourceId(R.styleable.ChipsView_cv_icon_delete, R.drawable.ic_close_24dp); 188 | mChipsHintText = a.getString(R.styleable.ChipsView_cv_text_hint); 189 | mChipsMargin = a.getDimensionPixelSize(R.styleable.ChipsView_cv_chips_margin, 0); 190 | } finally { 191 | a.recycle(); 192 | } 193 | } 194 | 195 | private void init() { 196 | mDensity = getResources().getDisplayMetrics().density; 197 | 198 | mChipsContainer = new RelativeLayout(getContext()); 199 | addView(mChipsContainer); 200 | 201 | // Dummy item to prevent AutoCompleteTextView from receiving focus 202 | LinearLayout linearLayout = new LinearLayout(getContext()); 203 | ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(0, 0); 204 | linearLayout.setLayoutParams(params); 205 | linearLayout.setFocusable(true); 206 | linearLayout.setFocusableInTouchMode(true); 207 | 208 | mChipsContainer.addView(linearLayout); 209 | 210 | mEditText = new ChipsEditText(getContext(), this); 211 | RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 212 | layoutParams.topMargin = (int) (SPACING_TOP * mDensity); 213 | layoutParams.bottomMargin = (int) (SPACING_BOTTOM * mDensity) + mVerticalSpacing; 214 | mEditText.setLayoutParams(layoutParams); 215 | mEditText.setMinHeight((int) (CHIP_HEIGHT * mDensity)); 216 | mEditText.setPadding(0, 0, 0, 0); 217 | mEditText.setLineSpacing(mVerticalSpacing, (CHIP_HEIGHT * mDensity) / mEditText.getLineHeight()); 218 | mEditText.setBackgroundColor(Color.argb(0, 0, 0, 0)); 219 | mEditText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_UNSPECIFIED); 220 | mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_TEXT_FLAG_MULTI_LINE); 221 | mEditText.setHint(mChipsHintText); 222 | 223 | mChipsContainer.addView(mEditText); 224 | 225 | mRootChipsLayout = new ChipsVerticalLinearLayout(getContext(), mVerticalSpacing); 226 | mRootChipsLayout.setOrientation(LinearLayout.VERTICAL); 227 | mRootChipsLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 228 | mRootChipsLayout.setPadding(0, (int) (SPACING_TOP * mDensity), 0, 0); 229 | mChipsContainer.addView(mRootChipsLayout); 230 | 231 | initListener(); 232 | 233 | if (isInEditMode()) { 234 | // preview chips 235 | LinearLayout editModeLinLayout = new LinearLayout(getContext()); 236 | editModeLinLayout.setOrientation(LinearLayout.HORIZONTAL); 237 | mChipsContainer.addView(editModeLinLayout); 238 | 239 | View view = new Chip("Test Chip", null, new Contact(null, null, "Test", "asd@asd.de", null)).getView(); 240 | view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 241 | editModeLinLayout.addView(view); 242 | 243 | View view2 = new Chip("Indelible", null, new Contact(null, null, "Test", "asd@asd.de", null), true).getView(); 244 | view2.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 245 | editModeLinLayout.addView(view2); 246 | } 247 | } 248 | 249 | private void initListener() { 250 | mChipsContainer.setOnClickListener(new OnClickListener() { 251 | @Override 252 | public void onClick(View v) { 253 | mEditText.requestFocus(); 254 | unselectAllChips(); 255 | } 256 | }); 257 | 258 | mEditTextListener = new EditTextListener(); 259 | mEditText.addTextChangedListener(mEditTextListener); 260 | mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { 261 | @Override 262 | public void onFocusChange(View v, boolean hasFocus) { 263 | if (hasFocus) { 264 | unselectAllChips(); 265 | } 266 | } 267 | }); 268 | } 269 | // 270 | 271 | // 272 | public void addChip(String displayName, String avatarUrl, Contact contact) { 273 | addChip(displayName, Uri.parse(avatarUrl), contact); 274 | } 275 | 276 | public void addChip(String displayName, Uri avatarUrl, Contact contact) { 277 | addChip(displayName, avatarUrl, contact, false); 278 | mEditText.setText(""); 279 | addLeadingMarginSpan(); 280 | } 281 | 282 | public void addChip(String displayName, Uri avatarUrl, Contact contact, boolean isIndelible) { 283 | Chip chip = new Chip(displayName, avatarUrl, contact, isIndelible); 284 | mChipList.add(chip); 285 | if (mChipsListener != null) { 286 | mChipsListener.onChipAdded(chip); 287 | } 288 | 289 | mEditText.setHint(null); 290 | 291 | onChipsChanged(true); 292 | post(new Runnable() { 293 | @Override 294 | public void run() { 295 | fullScroll(View.FOCUS_DOWN); 296 | } 297 | }); 298 | } 299 | 300 | public void setTypeface(@NonNull Typeface typeface) { 301 | this.mTypeface = typeface; 302 | if (mEditText != null) { 303 | mEditText.setTypeface(mTypeface); 304 | } 305 | } 306 | 307 | /** 308 | * Use Initials instead of the person icon. 309 | * 310 | * @param textSize in SP 311 | * @param initialsTypeface Nullable typeface 312 | */ 313 | public void useInitials(int textSize, @Nullable Typeface initialsTypeface, @ColorInt int textColor) { 314 | this.mUseInitials = true; 315 | this.mInitialsTextSize = textSize; 316 | this.mInitialsTypeface = initialsTypeface; 317 | this.mInitialsTextColor = textColor; 318 | } 319 | 320 | public void clearText() { 321 | mEditText.setText(""); 322 | onChipsChanged(true); 323 | } 324 | 325 | @NonNull 326 | public List getChips() { 327 | return Collections.unmodifiableList(mChipList); 328 | } 329 | 330 | public boolean removeChipBy(Contact contact) { 331 | for (int i = 0; i < mChipList.size(); i++) { 332 | if (mChipList.get(i).mContact != null && mChipList.get(i).mContact.equals(contact)) { 333 | mChipList.remove(i); 334 | if(mChipList.isEmpty()) { 335 | mEditText.setHint(mChipsHintText); 336 | } 337 | onChipsChanged(true); 338 | return true; 339 | } 340 | } 341 | return false; 342 | } 343 | 344 | public Contact tryToRecognizeAddress() { 345 | String text = mEditText.getText().toString(); 346 | if (!TextUtils.isEmpty(text)) { 347 | if (Common.isValidEmail(text)) { 348 | return new Contact(text, "", null, text, null); 349 | } 350 | } 351 | return null; 352 | } 353 | 354 | public void setChipsListener(ChipsListener chipsListener) { 355 | this.mChipsListener = chipsListener; 356 | } 357 | 358 | public void setChipsValidator(ChipValidator chipsValidator) { 359 | mChipsValidator = chipsValidator; 360 | } 361 | 362 | public EditText getEditText() { 363 | return mEditText; 364 | } 365 | // 366 | 367 | // 368 | /** 369 | * rebuild all chips and place them right 370 | */ 371 | private void onChipsChanged(final boolean moveCursor) { 372 | ChipsVerticalLinearLayout.TextLineParams textLineParams = mRootChipsLayout.onChipsChanged(mChipList); 373 | 374 | // if null then run another layout pass 375 | if (textLineParams == null) { 376 | post(new Runnable() { 377 | @Override 378 | public void run() { 379 | onChipsChanged(moveCursor); 380 | } 381 | }); 382 | return; 383 | } 384 | 385 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mEditText.getLayoutParams(); 386 | params.topMargin = (int) ((SPACING_TOP + textLineParams.row * CHIP_HEIGHT) * mDensity) + textLineParams.row * mVerticalSpacing; 387 | mEditText.setLayoutParams(params); 388 | addLeadingMarginSpan(textLineParams.lineMargin + mChipsMargin * textLineParams.chipsCount); 389 | if (moveCursor) { 390 | mEditText.setSelection(mEditText.length()); 391 | } 392 | } 393 | 394 | private void addLeadingMarginSpan(int margin) { 395 | Spannable spannable = mEditText.getText(); 396 | if (mCurrentEditTextSpan != null) { 397 | spannable.removeSpan(mCurrentEditTextSpan); 398 | } 399 | mCurrentEditTextSpan = new android.text.style.LeadingMarginSpan.LeadingMarginSpan2.Standard(margin, 0); 400 | spannable.setSpan(mCurrentEditTextSpan, 0, 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 401 | 402 | mEditText.setText(spannable); 403 | } 404 | 405 | private void addLeadingMarginSpan() { 406 | Spannable spannable = mEditText.getText(); 407 | if (mCurrentEditTextSpan != null) { 408 | spannable.removeSpan(mCurrentEditTextSpan); 409 | } 410 | spannable.setSpan(mCurrentEditTextSpan, 0, 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 411 | 412 | mEditText.setText(spannable); 413 | } 414 | 415 | /** 416 | * return true if the text should be deleted 417 | */ 418 | private boolean onEnterPressed(String text) { 419 | boolean shouldDeleteText = true; 420 | if (text != null && text.length() > 0) { 421 | 422 | if (Common.isValidEmail(text)) { 423 | onEmailRecognized(text); 424 | } else { 425 | shouldDeleteText = onNonEmailRecognized(text); 426 | } 427 | if (shouldDeleteText) { 428 | mEditText.setSelection(0); 429 | } 430 | } 431 | return shouldDeleteText; 432 | } 433 | 434 | private void onEmailRecognized(String email) { 435 | onEmailRecognized(new Contact(email, "", null, email, null)); 436 | } 437 | 438 | private void onEmailRecognized(Contact contact) { 439 | Chip chip = new Chip(contact.getDisplayName(), null, contact); 440 | mChipList.add(chip); 441 | if (mChipsListener != null) { 442 | mChipsListener.onChipAdded(chip); 443 | } 444 | post(new Runnable() { 445 | @Override 446 | public void run() { 447 | onChipsChanged(true); 448 | } 449 | }); 450 | } 451 | 452 | private boolean onNonEmailRecognized(String text) { 453 | if (mChipsListener != null) { 454 | return mChipsListener.onInputNotRecognized(text); 455 | } 456 | return true; 457 | } 458 | 459 | private void selectOrDeleteLastChip() { 460 | if (mChipList.size() > 0) { 461 | onChipInteraction(mChipList.size() - 1); 462 | } 463 | } 464 | 465 | private void onChipInteraction(int position) { 466 | try { 467 | Chip chip = mChipList.get(position); 468 | if (chip != null) { 469 | onChipInteraction(chip, true); 470 | } 471 | } catch (IndexOutOfBoundsException e) { 472 | Log.e(TAG, "Out of bounds", e); 473 | } 474 | } 475 | 476 | private void onChipInteraction(Chip chip, boolean nameClicked) { 477 | unselectChipsExcept(chip); 478 | if (chip.isSelected()) { 479 | mChipList.remove(chip); 480 | if (mChipsListener != null) { 481 | mChipsListener.onChipDeleted(chip); 482 | } 483 | onChipsChanged(true); 484 | if (nameClicked) { 485 | mEditText.setText(chip.getContact().getEmailAddress()); 486 | addLeadingMarginSpan(); 487 | mEditText.requestFocus(); 488 | mEditText.setSelection(mEditText.length()); 489 | } 490 | } else { 491 | chip.setSelected(true); 492 | onChipsChanged(false); 493 | } 494 | } 495 | 496 | private void unselectChipsExcept(Chip rootChip) { 497 | for (Chip chip : mChipList) { 498 | if (chip != rootChip) { 499 | chip.setSelected(false); 500 | } 501 | } 502 | onChipsChanged(false); 503 | } 504 | 505 | private void unselectAllChips() { 506 | unselectChipsExcept(null); 507 | } 508 | // 509 | 510 | // 511 | @Override 512 | public InputConnection getInputConnection(InputConnection target) { 513 | return new KeyInterceptingInputConnection(target); 514 | } 515 | // 516 | 517 | // 518 | private class EditTextListener implements TextWatcher { 519 | 520 | private boolean mIsPasteTextChange = false; 521 | 522 | @Override 523 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 524 | } 525 | 526 | @Override 527 | public void onTextChanged(CharSequence s, int start, int before, int count) { 528 | if (count > 1) { 529 | mIsPasteTextChange = true; 530 | } 531 | } 532 | 533 | @Override 534 | public void afterTextChanged(Editable s) { 535 | if (mIsPasteTextChange) { 536 | mIsPasteTextChange = false; 537 | // todo handle copy/paste text here 538 | 539 | } else { 540 | // no paste text change 541 | if (s.toString().contains("\n")) { 542 | String text = s.toString(); 543 | text = text.replace("\n", ""); 544 | while (text.contains(" ")) { 545 | text = text.replace(" ", " "); 546 | } 547 | if (text.length() > 1) { 548 | s.clear(); 549 | if (!onEnterPressed(text)) { 550 | s.append(text); 551 | } 552 | } else { 553 | s.clear(); 554 | s.append(text); 555 | } 556 | } 557 | } 558 | if (mChipsListener != null) { 559 | mChipsListener.onTextChanged(s); 560 | } 561 | } 562 | } 563 | 564 | private class KeyInterceptingInputConnection extends InputConnectionWrapper { 565 | 566 | public KeyInterceptingInputConnection(InputConnection target) { 567 | super(target, true); 568 | } 569 | 570 | @Override 571 | public boolean commitText(CharSequence text, int newCursorPosition) { 572 | return super.commitText(text, newCursorPosition); 573 | } 574 | 575 | @Override 576 | public boolean sendKeyEvent(KeyEvent event) { 577 | if (mEditText.length() == 0) { 578 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 579 | if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 580 | selectOrDeleteLastChip(); 581 | return true; 582 | } 583 | } 584 | } 585 | if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { 586 | mEditText.append("\n"); 587 | return true; 588 | } 589 | 590 | return super.sendKeyEvent(event); 591 | } 592 | 593 | @Override 594 | public boolean deleteSurroundingText(int beforeLength, int afterLength) { 595 | // magic: in latest Android, deleteSurroundingText(1, 0) will be called for backspace 596 | if (mEditText.length() == 0 && beforeLength == 1 && afterLength == 0) { 597 | // backspace 598 | return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) 599 | && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); 600 | } 601 | 602 | return super.deleteSurroundingText(beforeLength, afterLength); 603 | } 604 | } 605 | 606 | public class Chip implements OnClickListener { 607 | 608 | private static final int MAX_LABEL_LENGTH = 30; 609 | 610 | private String mLabel; 611 | private final Uri mPhotoUri; 612 | private final Contact mContact; 613 | private final boolean mIsIndelible; 614 | 615 | private RelativeLayout mView; 616 | private View mIconWrapper; 617 | private TextView mTextView; 618 | private TextView mInitials; 619 | 620 | private ImageView mAvatarView; 621 | private ImageView mPersonIcon; 622 | private ImageView mCloseIcon; 623 | 624 | private ImageView mErrorIcon; 625 | 626 | private boolean mIsSelected = false; 627 | 628 | public Chip(String label, Uri photoUri, Contact contact) { 629 | this(label, photoUri, contact, false); 630 | } 631 | 632 | public Chip(String label, Uri photoUri, Contact contact, boolean isIndelible) { 633 | this.mLabel = label; 634 | this.mPhotoUri = photoUri; 635 | this.mContact = contact; 636 | this.mIsIndelible = isIndelible; 637 | 638 | if (mLabel == null) { 639 | mLabel = contact.getEmailAddress(); 640 | } 641 | 642 | if (mLabel.length() > MAX_LABEL_LENGTH) { 643 | mLabel = mLabel.substring(0, MAX_LABEL_LENGTH) + "..."; 644 | } 645 | } 646 | 647 | public View getView() { 648 | if (mView == null) { 649 | mView = (RelativeLayout) inflate(getContext(), R.layout.chips_view, null); 650 | 651 | // Layout Params + margins 652 | RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, (int) (CHIP_HEIGHT * mDensity)); 653 | layoutParams.setMargins(0, 0, mChipsMargin, 0); 654 | mView.setLayoutParams(layoutParams); 655 | 656 | mAvatarView = (ImageView) mView.findViewById(R.id.ri_ch_avatar); 657 | mIconWrapper = mView.findViewById(R.id.rl_ch_avatar); 658 | mTextView = (TextView) mView.findViewById(R.id.tv_ch_name); 659 | mInitials = (TextView) mView.findViewById(R.id.tv_ch_initials); 660 | mPersonIcon = (ImageView) mView.findViewById(R.id.iv_ch_person); 661 | mCloseIcon = (ImageView) mView.findViewById(R.id.iv_ch_close); 662 | 663 | mErrorIcon = (ImageView) mView.findViewById(R.id.iv_ch_error); 664 | 665 | // set initial res & attrs 666 | if (mTypeface != null) { 667 | mTextView.setTypeface(mTypeface); 668 | } 669 | mView.setBackgroundResource(mChipsBgRes); 670 | if (mIsIndelible) { 671 | ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColorIndelible); 672 | } else { 673 | ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColor); 674 | } 675 | mIconWrapper.setBackgroundResource(R.drawable.circle); 676 | if (mIsIndelible) { 677 | mTextView.setTextColor(mChipsTextColorIndelible); 678 | } else { 679 | mTextView.setTextColor(mChipsTextColor); 680 | } 681 | 682 | // set icon resources 683 | mPersonIcon.setImageResource(mChipsPlaceholderResId); 684 | if (mChipsPlaceholderTint != 0) { 685 | mPersonIcon.setColorFilter(mChipsPlaceholderTint, PorterDuff.Mode.SRC_ATOP); 686 | } 687 | mCloseIcon.setBackgroundResource(mChipsDeleteResId); 688 | 689 | // USE INITIALS INSTEAD OF PERSON ICON 690 | if (mUseInitials) { 691 | mPersonIcon.setVisibility(GONE); 692 | mInitials.setVisibility(VISIBLE); 693 | if (mInitialsTypeface != null) { 694 | mInitials.setTypeface(mInitialsTypeface); 695 | } 696 | if (mInitialsTextColor != 0) { 697 | mInitials.setTextColor(mInitialsTextColor); 698 | } 699 | if (mInitialsTextSize != 0) { 700 | mInitials.setTextSize(TypedValue.COMPLEX_UNIT_SP, mInitialsTextSize); 701 | } 702 | } else { 703 | mPersonIcon.setVisibility(VISIBLE); 704 | mInitials.setVisibility(GONE); 705 | } 706 | 707 | mView.setOnClickListener(this); 708 | mIconWrapper.setOnClickListener(this); 709 | } 710 | updateViews(); 711 | return mView; 712 | } 713 | 714 | private void updateViews() { 715 | mTextView.setText(mLabel); 716 | if (mUseInitials) { 717 | mInitials.setText(getInitials()); 718 | } 719 | if (mTypeface != null) { 720 | mTextView.setTypeface(mTypeface); 721 | } 722 | if (mPhotoUri != null) { 723 | Picasso.with(getContext()) 724 | .load(mPhotoUri) 725 | .noPlaceholder() 726 | .into(mAvatarView, new Callback() { 727 | @Override 728 | public void onSuccess() { 729 | mPersonIcon.setVisibility(View.INVISIBLE); 730 | } 731 | 732 | @Override 733 | public void onError() { 734 | 735 | } 736 | }); 737 | } 738 | if (isSelected()) { 739 | if (mChipsValidator != null && !mChipsValidator.isValid(mContact)) { 740 | // not valid & show error 741 | ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColorErrorClicked); 742 | mTextView.setTextColor(mChipsTextColorErrorClicked); 743 | mIconWrapper.getBackground().setColorFilter(mChipsColorErrorClicked, PorterDuff.Mode.SRC_ATOP); 744 | mErrorIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP); 745 | } else { 746 | ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColorClicked); 747 | mTextView.setTextColor(mChipsTextColorClicked); 748 | mIconWrapper.getBackground().setColorFilter(mChipsColorClicked, PorterDuff.Mode.SRC_ATOP); 749 | } 750 | if (mUseInitials) { 751 | mInitials.animate().alpha(0.0f).setDuration(200).start(); 752 | } else { 753 | mPersonIcon.animate().alpha(0.0f).setDuration(200).start(); 754 | } 755 | mAvatarView.animate().alpha(0.0f).setDuration(200).start(); 756 | mCloseIcon.animate().alpha(1f).setDuration(200).setStartDelay(100).start(); 757 | 758 | } else { 759 | if (mChipsValidator != null && !mChipsValidator.isValid(mContact)) { 760 | // not valid & show error 761 | mErrorIcon.setVisibility(View.VISIBLE); 762 | mErrorIcon.setColorFilter(null); 763 | } else { 764 | mErrorIcon.setVisibility(View.GONE); 765 | } 766 | if (mIsIndelible) { 767 | ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColorIndelible); 768 | mTextView.setTextColor(mChipsTextColorIndelible); 769 | } else { 770 | ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColor); 771 | mTextView.setTextColor(mChipsTextColor); 772 | } 773 | mIconWrapper.getBackground().setColorFilter(mChipsColor, PorterDuff.Mode.SRC_ATOP); 774 | 775 | if (mUseInitials) { 776 | mInitials.animate().alpha(1f).setDuration(200).setStartDelay(100).start(); 777 | } else { 778 | mPersonIcon.animate().alpha(1f).setDuration(200).setStartDelay(100).start(); 779 | } 780 | mAvatarView.animate().alpha(1f).setDuration(200).setStartDelay(100).start(); 781 | mCloseIcon.animate().alpha(0.0f).setDuration(200).start(); 782 | } 783 | } 784 | 785 | @NonNull 786 | private String getInitials() { 787 | if (mLabel != null) { 788 | if (mLabel.trim().contains(" ")) { 789 | String[] split = mLabel.trim().split(" "); 790 | return String.format("%s%s", String.valueOf(split[0].charAt(0)), String.valueOf(split[split.length - 1].charAt(0))); 791 | } else { 792 | return String.valueOf(mLabel.charAt(0)); 793 | } 794 | } else { 795 | return ""; 796 | } 797 | } 798 | 799 | @Override 800 | public void onClick(View v) { 801 | mEditText.clearFocus(); 802 | if (v.getId() == mView.getId()) { 803 | onChipInteraction(this, true); 804 | } else { 805 | onChipInteraction(this, false); 806 | } 807 | } 808 | 809 | public boolean isSelected() { 810 | return mIsSelected; 811 | } 812 | 813 | public void setSelected(boolean isSelected) { 814 | if (mIsIndelible) { 815 | return; 816 | } 817 | this.mIsSelected = isSelected; 818 | } 819 | 820 | public Contact getContact() { 821 | return mContact; 822 | } 823 | 824 | @Override 825 | public boolean equals(Object o) { 826 | if (mContact != null && o instanceof Contact) { 827 | return mContact.equals(o); 828 | } 829 | return super.equals(o); 830 | } 831 | 832 | @Override 833 | public String toString() { 834 | return "{" 835 | + "[Contact: " + mContact + "]" 836 | + "[Label: " + mLabel + "]" 837 | + "[PhotoUri: " + mPhotoUri + "]" 838 | + "[IsIndelible" + mIsIndelible + "]" 839 | + "}" 840 | ; 841 | } 842 | } 843 | 844 | public interface ChipsListener { 845 | void onChipAdded(Chip chip); 846 | 847 | void onChipDeleted(Chip chip); 848 | 849 | void onTextChanged(CharSequence text); 850 | 851 | /** 852 | * return true to delete the invalid text. 853 | */ 854 | boolean onInputNotRecognized(String text); 855 | } 856 | 857 | public static abstract class ChipValidator { 858 | public abstract boolean isValid(Contact contact); 859 | } 860 | // 861 | } 862 | -------------------------------------------------------------------------------- /library/src/main/java/com/doodle/android/chips/model/Contact.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Doodle AG. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.doodle.android.chips.model; 18 | 19 | import android.net.Uri; 20 | import android.support.annotation.NonNull; 21 | import android.support.annotation.Nullable; 22 | import android.text.TextUtils; 23 | 24 | import java.io.Serializable; 25 | 26 | public class Contact implements Comparable, Serializable { 27 | 28 | @Nullable 29 | private final String mFirstName; 30 | 31 | @Nullable 32 | private final String mLastName; 33 | 34 | @NonNull 35 | private final String mEmailAddress; 36 | 37 | @Nullable 38 | private transient final Uri mAvatarUri; 39 | 40 | @NonNull 41 | private final String mDisplayName; 42 | 43 | @NonNull 44 | private final String mInitials; 45 | 46 | public Contact(@Nullable String firstName, @Nullable String lastName, @Nullable String displayName, @NonNull String emailAddress, @Nullable Uri avatarUri) { 47 | mFirstName = firstName; 48 | mLastName = lastName; 49 | mAvatarUri = avatarUri; 50 | mEmailAddress = emailAddress; 51 | 52 | if (!TextUtils.isEmpty(displayName)) { 53 | mDisplayName = displayName; 54 | } else if (TextUtils.isEmpty(mFirstName)) { 55 | if (TextUtils.isEmpty(mLastName)) { 56 | mDisplayName = mEmailAddress; 57 | } else { 58 | mDisplayName = mLastName; 59 | } 60 | } else if (TextUtils.isEmpty(mLastName)) { 61 | mDisplayName = mFirstName; 62 | } else { 63 | mDisplayName = mFirstName + " " + mLastName; 64 | } 65 | 66 | StringBuilder initialsBuilder = new StringBuilder(); 67 | if (!TextUtils.isEmpty(mFirstName)) { 68 | initialsBuilder.append(Character.toUpperCase(mFirstName.charAt(0))); 69 | } 70 | if (!TextUtils.isEmpty(mLastName)) { 71 | initialsBuilder.append(Character.toUpperCase(mLastName.charAt(0))); 72 | } 73 | mInitials = initialsBuilder.toString(); 74 | } 75 | 76 | @Nullable 77 | public String getFirstName() { 78 | return mFirstName; 79 | } 80 | 81 | @Nullable 82 | public String getLastName() { 83 | return mLastName; 84 | } 85 | 86 | @NonNull 87 | public String getEmailAddress() { 88 | return mEmailAddress; 89 | } 90 | 91 | @Nullable 92 | public Uri getAvatarUri() { 93 | return mAvatarUri; 94 | } 95 | 96 | @NonNull 97 | public String getDisplayName() { 98 | return mDisplayName; 99 | } 100 | 101 | @NonNull 102 | public String getInitials() { 103 | return mInitials; 104 | } 105 | 106 | @Override 107 | public int compareTo(final Contact another) { 108 | 109 | if (another == null) { 110 | return 1; 111 | } 112 | 113 | // compare whatever is the first visible component of the name 114 | String myString; 115 | if (mDisplayName != null) { 116 | myString = mDisplayName; 117 | } else if (mFirstName != null) { 118 | myString = mFirstName; 119 | } else { 120 | myString = mLastName; 121 | } 122 | 123 | String otherString; 124 | if (another.mDisplayName != null) { 125 | otherString = another.mDisplayName; 126 | } else if (another.mFirstName != null) { 127 | otherString = another.mFirstName; 128 | } else { 129 | otherString = another.mLastName; 130 | } 131 | 132 | int diff = compare(myString, otherString); 133 | if (diff != 0) { 134 | return diff; 135 | } 136 | 137 | if (another.mFirstName == null && mFirstName != null) { 138 | return 1; 139 | } 140 | if (another.mFirstName != null && mFirstName == null) { 141 | return -1; 142 | } 143 | 144 | if (another.mFirstName != null && mFirstName != null) { 145 | // both have first names, so we didn't yet compare last names 146 | diff = compare(mLastName, another.mLastName); 147 | if (diff != 0) { 148 | return diff; 149 | } 150 | } 151 | 152 | return mEmailAddress.compareTo(another.mEmailAddress); 153 | } 154 | 155 | private int compare(String myString, String otherString) { 156 | boolean isMineBlank = TextUtils.isEmpty(myString); 157 | boolean isOtherBlank = TextUtils.isEmpty(otherString); 158 | if (isMineBlank && isOtherBlank) { 159 | return 0; 160 | } 161 | if (isMineBlank) { 162 | return 1; 163 | } 164 | if (isOtherBlank) { 165 | return -1; 166 | } 167 | return myString.toLowerCase().compareTo(otherString.toLowerCase()); 168 | } 169 | 170 | public boolean matches(CharSequence searchString) { 171 | String lowerCaseSearchString = searchString.toString().toLowerCase(); 172 | return (mFirstName != null && mFirstName.toLowerCase().contains(lowerCaseSearchString)) || 173 | (mLastName != null && mLastName.toLowerCase().contains(lowerCaseSearchString)) || 174 | mEmailAddress.toLowerCase().contains(lowerCaseSearchString); 175 | } 176 | 177 | 178 | @Override 179 | public boolean equals(final Object o) { 180 | if (this == o) return true; 181 | if (o == null || getClass() != o.getClass()) return false; 182 | 183 | Contact contact = (Contact) o; 184 | 185 | if (!mEmailAddress.equals(contact.mEmailAddress)) return false; 186 | 187 | return true; 188 | } 189 | 190 | @Override 191 | public int hashCode() { 192 | return mEmailAddress.hashCode(); 193 | } 194 | 195 | @Override 196 | public String toString() { 197 | return "Contact{" + 198 | "mFirstName='" + mFirstName + '\'' + 199 | ", mLastName='" + mLastName + '\'' + 200 | ", mEmailAddress='" + mEmailAddress + '\'' + 201 | ", mAvatarUri=" + mAvatarUri + 202 | ", mDisplayName='" + mDisplayName + '\'' + 203 | ", mInitials='" + mInitials + '\'' + 204 | '}'; 205 | } 206 | } -------------------------------------------------------------------------------- /library/src/main/java/com/doodle/android/chips/util/Common.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Doodle AG. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.doodle.android.chips.util; 18 | 19 | import android.text.TextUtils; 20 | 21 | public class Common { 22 | 23 | public static boolean isValidEmail(CharSequence target) { 24 | return !TextUtils.isEmpty(target) && android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/src/main/java/com/doodle/android/chips/views/ChipsEditText.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Doodle AG. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.doodle.android.chips.views; 18 | 19 | import android.content.Context; 20 | import android.support.v7.widget.AppCompatEditText; 21 | import android.view.inputmethod.EditorInfo; 22 | import android.view.inputmethod.InputConnection; 23 | 24 | public class ChipsEditText extends AppCompatEditText { 25 | 26 | private InputConnectionWrapperInterface mInputConnectionWrapperInterface; 27 | 28 | public ChipsEditText(Context context, InputConnectionWrapperInterface inputConnectionWrapperInterface) { 29 | super(context); 30 | this.mInputConnectionWrapperInterface = inputConnectionWrapperInterface; 31 | } 32 | 33 | @Override 34 | public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 35 | if (mInputConnectionWrapperInterface != null) { 36 | return mInputConnectionWrapperInterface.getInputConnection(super.onCreateInputConnection(outAttrs)); 37 | } 38 | 39 | return super.onCreateInputConnection(outAttrs); 40 | } 41 | 42 | public interface InputConnectionWrapperInterface { 43 | InputConnection getInputConnection(InputConnection target); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /library/src/main/java/com/doodle/android/chips/views/ChipsVerticalLinearLayout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Doodle AG. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.doodle.android.chips.views; 18 | 19 | import android.content.Context; 20 | import android.view.View; 21 | import android.widget.LinearLayout; 22 | 23 | import com.doodle.android.chips.ChipsView; 24 | 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | public class ChipsVerticalLinearLayout extends LinearLayout { 29 | 30 | private List mLineLayouts = new ArrayList<>(); 31 | 32 | private float mDensity; 33 | private int mRowSpacing; 34 | 35 | public ChipsVerticalLinearLayout(Context context, int rowSpacing) { 36 | super(context); 37 | 38 | mDensity = getResources().getDisplayMetrics().density; 39 | mRowSpacing = rowSpacing; 40 | 41 | init(); 42 | } 43 | 44 | private void init() { 45 | setOrientation(VERTICAL); 46 | } 47 | 48 | public TextLineParams onChipsChanged(List chips) { 49 | clearChipsViews(); 50 | 51 | int width = getWidth(); 52 | if (width == 0) { 53 | return null; 54 | } 55 | int widthSum = 0; 56 | int chipsCount = 0; 57 | int rowCounter = 0; 58 | 59 | LinearLayout ll = createHorizontalView(); 60 | 61 | for (ChipsView.Chip chip : chips) { 62 | View view = chip.getView(); 63 | view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 64 | 65 | // if width exceed current width. create a new LinearLayout 66 | if (widthSum + view.getMeasuredWidth() > width) { 67 | rowCounter++; 68 | widthSum = 0; 69 | chipsCount = 0; 70 | ll = createHorizontalView(); 71 | } 72 | 73 | widthSum += view.getMeasuredWidth(); 74 | chipsCount++; 75 | ll.addView(view); 76 | } 77 | 78 | // check if there is enough space left 79 | if (width - widthSum < width * 0.1f) { 80 | widthSum = 0; 81 | chipsCount = 0; 82 | rowCounter++; 83 | } 84 | if (width == 0) { 85 | rowCounter = 0; 86 | } 87 | return new TextLineParams(rowCounter, widthSum, chipsCount); 88 | } 89 | 90 | private LinearLayout createHorizontalView() { 91 | LinearLayout ll = new LinearLayout(getContext()); 92 | ll.setPadding(0, 0, 0, mRowSpacing); 93 | ll.setOrientation(HORIZONTAL); 94 | addView(ll); 95 | mLineLayouts.add(ll); 96 | return ll; 97 | } 98 | 99 | private void clearChipsViews() { 100 | for (LinearLayout linearLayout : mLineLayouts) { 101 | linearLayout.removeAllViews(); 102 | } 103 | mLineLayouts.clear(); 104 | removeAllViews(); 105 | } 106 | 107 | public static class TextLineParams { 108 | public int row; 109 | public int lineMargin; 110 | public int chipsCount = 0; 111 | 112 | public TextLineParams(int row, int lineMargin, int chipsCount) { 113 | this.row = row; 114 | this.lineMargin = lineMargin; 115 | this.chipsCount = chipsCount; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-v21/btn_trans_base10.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/btn_trans_base10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chip_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_close_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_error_red_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_person_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chips_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 25 | 26 | 35 | 36 | 43 | 44 | 52 | 53 | 54 | 65 | 66 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /library/src/main/res/layout/dialog_chips_email.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 17 | 28 | 29 | 30 | 42 | 43 | 44 | 52 | 53 | 54 |