diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..39fb081a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..943201a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 pchmn + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..211e5553 --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ +# MaterialChipsInput + +Implementation of Material Design [Chips](https://material.io/guidelines/components/chips.html) component for Android. The library provides two views : [`ChipsInput`](#chipsinput) and [`ChipView`](#chipview). + +[![Release](https://jitpack.io/v/pchmn/MaterialChipsInput.svg)](https://jitpack.io/#pchmn/MaterialChipsInput) + +Demo + +## Sample APK +[sample-v1.0.0.apk](https://github.com/pchmn/MaterialChipsInput/raw/master/docs/material-chips-input-sample-v1.0.0.apk) + +## Setup + +To use this library your `minSdkVersion` must be >= 15. + +In your project level build.gradle : +```java +allprojects { + repositories { + ... + maven { url "https://jitpack.io" } + } +} +``` + +In your app level build.gradle : +```java +dependencies { + compile 'com.github.pchmn:MaterialChipsInput:1.0.0' +} +``` +

+## ChipsInput +This view implements the Material Design [Contact chips component](https://material.io/guidelines/components/chips.html#chips-contact-chips). + +It is composed of a collection of chips (`ChipView`) and an input (`EditText`). Touching a chip open a full detailed view (if non disable). The [GIF](#materialchipsinput) above describes the behavior of the `ChipsInput` view. + +But everything is configurable (optional avatar icon, optional full detailed view, ...) so you can use the `ChipsInput` view for non contact chips. +### Basic Usage + +#### XML +Use the ChipsInput view in your layout and customize it ([see](#chipsinput-attributes) all attributes) : + +```xml + +``` + +#### Suggestions +You can pass a `List` object, which represents your suggestions, to the `ChipsInput` view, so it will work as a +`MultiAutoCompleteTextView` : + +##### 1. Create a class that implements `ChipInterface` (or use directly the [`Chip`](https://github.com/pchmn/MaterialChipsInput/blob/master/library/src/main/java/com/pchmn/materialchips/model/Chip.java) class included in the library) : +```java +public class ContactChip implements ChipInterface { + ... +} +``` + +##### 2. Then in your activity, or anything else, build your suggestion list of `ContactChip` (or `Chip`) and pass it to the `ChipsInput` view : +```java +// get ChipsInput view +ChipsInput chipsInput = (ChipsInput) findViewById(R.id.chips_input); + +// build the ContactChip list +List contactList = new ArrayList<>(); +contactList.add(new ContactChip()); +... + +// pass the ContactChip list +chipsInput.setFilterableList(contactList); +``` + +#### Get the selected list +When you want you can get the current list of chips selected by the user : +```java +// get the list +List contactsSelected = (List) chipsInput.getSelectedChipList(); +``` + +That's it, there is nothing more to do. +

+### Advanced Usage +#### ChipsListener +The `ChipsInput` view provides a listener to interact with the input : +```java +chipsInput.addChipsListener(new ChipsInput.ChipsListener() { + @Override + public void onChipAdded(ChipInterface chip, int newSize) { + // chip added + // newSize is the size of the updated selected chip list + } + + @Override + public void onChipRemoved(ChipInterface chip, int newSize) { + // chip removed + // newSize is the size of the updated selected chip list + } + + @Override + public void onTextChanged(CharSequence text) { + // text changed + } + }); +``` + +#### Add and remove chips manually +You don't have to pass a `List` to the `ChipsInput` view and you can do the trick manually. Thanks to the `ChipsListener` you can be notified when the user is typing and do your own work. + +```java +ChipsInput chipsInput = (ChipsInput) findViewById(R.id.chips_input); +``` + +##### Add a chip +There are multiple implementations : +```java +chipsInput.addChip(ChipInterface chip); +// or +chipsInput.addChip(Object id, Drawable icon, String label, String info); +// or +chipsInput.addChip(Drawable icon, String label, String info); +// or +chipsInput.addChip(Object id, Uri iconUri, String label, String info); +// or +chipsInput.addChip(Uri iconUri, String label, String info); +// or +chipsInput.addChip(String label, String info); +``` + +##### Remove a chip +There are multiple implementations : +```java +chipsInput.removeChip(ChipInterface chip); +// or +chipsInput.removeChipById(Object id); +// or +chipsInput.removeChipByLabel(String label); +// or +chipsInput.removeChipByInfo(String info); +``` + +After you added or removed a chip the `ChipsListener` will be triggered. + +##### Get the selected list +When you want you can get the current list of chips selected by the user : +```java +// get the list +List contactsSelected = chipsInput.getSelectedChipList(); +``` + +### ChipsInput attributes + +Attribute | Type | Description | Default +--- | --- | --- | --- +`app:hint` | `string` | Hint of the input when there is no chip | null +`app:hintColor` | `color` | Hint color | android default +`app:textColor` | `color` | Text color when user types | android default +`app:maxRows` | `int` | Max rows of chips | 2 +`app:chip_labelColor` | `color` | Label color of the chips | android default +`app:chip_hasAvatarIcon` | `boolean` | Whether the chips have avatar icon or not | true +`app:chip_deletable` | `boolean` | Whether the chips are deletable (delete icon) or not | false +`app:chip_deleteIconColor` | `color` | Delete icon color of the chips | white/black +`app:chip_backgroundColor` | `color` | Background color of the chips | grey +`app:showChipDetailed` | `boolean` | Whether to show full detailed view or not when touching a chip | true +`app:chip_detailed_textColor` | `color` | Full detailed view text color | white/balck +`app:chip_detailed_backgroundColor` | `color` | Background color of the full detailed view | colorAccent +`app:chip_detailed_deleteIconColor` | `color` | Delete icon color of the full detailed view | white/black +`app:filterable_list_backgroundColor` | `color` | Background color of the filterable list of suggestions | white +`app:filterable_list_textColor` | `color` | Text color of the filterable list of suggestions | black + +

+## ChipView +This view implements the chip component according to the [Material Design guidelines](https://material.io/guidelines/components/chips.html#chips-usage) with configurable options (background color, text color, ...). + +Chips examples + +### Usage +```xml + + + + + + +``` + +### ChipView attributes + +Attribute | Type | Description | Default +--- | --- | --- | --- +`app:label` | `string` | Label of the chip | null +`app:labelColor` | `color` | Label color of the chip | android default +`app:hasAvatarIcon` | `boolean` | Whether the chip has avatar icon or not | false +`app:avatarIcon` | `drawable` | Avatar icon resource | null +`app:deletable` | `boolean` | Whether the chip is deletable (delete icon) or not | false +`app:deleteIconColor` | `color` | Delete icon color of the chip | grey +`app:backgroundColor` | `color` | Background color of the chip | grey + +### Listeners +```java +ChipView chip = (ChipView) findViewById(R.id.chip_view); +``` + +On chip click listener : +```java +chip.setOnChipClicked(new View.OnClickListener() { + @Override + public void onClick(View view) { + // handle click + } +}); +``` + +On delete button click listener : +```java +chip.setOnDeleteClicked(new View.OnClickListener() { + @Override + public void onClick(View view) { + // handle click + } +}); +``` +

+## Sample + +A sample app with some use cases of the library is available on this [link](https://github.com/pchmn/MaterialChipsInput/tree/master/sample) + +You can also download the sample APK [here](https://github.com/pchmn/MaterialChipsInput/raw/master/docs/material-chips-input-sample-v1.0.0.apk) + +## Credits + +* [Android Material Chips](https://github.com/DoodleScheduling/android-material-chips) +* [Material Chip View](https://github.com/robertlevonyan/materialChipView?utm_source=android-arsenal.com&utm_medium=referral&utm_campaign=5396) +* [ChipsLayoutManager](https://github.com/BelooS/ChipsLayoutManager) + +## License + +``` +Copyright 2017 pchmn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..64dc55d3 --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + classpath 'com.jakewharton:butterknife-gradle-plugin:8.5.1' + classpath 'me.tatarka:gradle-retrolambda:3.6.0' + classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' + } +} + +allprojects { + repositories { + jcenter() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/docs/Screenshot_20170413-164058.png b/docs/Screenshot_20170413-164058.png new file mode 100644 index 00000000..c25598a3 Binary files /dev/null and b/docs/Screenshot_20170413-164058.png differ diff --git a/docs/chips-examples.png b/docs/chips-examples.png new file mode 100644 index 00000000..57b7b930 Binary files /dev/null and b/docs/chips-examples.png differ diff --git a/docs/chips-input-illustration.png b/docs/chips-input-illustration.png new file mode 100644 index 00000000..2578a1af Binary files /dev/null and b/docs/chips-input-illustration.png differ diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100755 index 00000000..7e3c93a8 Binary files /dev/null and b/docs/demo.gif differ diff --git a/docs/demo2.gif b/docs/demo2.gif new file mode 100644 index 00000000..66004b3c Binary files /dev/null and b/docs/demo2.gif differ diff --git a/docs/material-chips-input-sample-v1.0.0.apk b/docs/material-chips-input-sample-v1.0.0.apk new file mode 100644 index 00000000..324c89ed Binary files /dev/null and b/docs/material-chips-input-sample-v1.0.0.apk differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..aac7c9b4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..7f3b2ff9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Mar 28 20:31:03 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 00000000..0fb0cc05 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.library' +apply plugin: 'com.jakewharton.butterknife' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 25 + versionCode 1 + versionName "1.0.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:25.3.0' + testCompile 'junit:junit:4.12' + + // recycler + compile 'com.android.support:recyclerview-v7:25.3.0' + compile 'com.beloo.widget:ChipsLayoutManager:0.3.7@aar' + + // circle image view + compile 'de.hdodenhof:circleimageview:2.1.0' + + // butter knife + compile 'com.jakewharton:butterknife:8.5.1' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' +} + +apply plugin: 'com.github.dcendents.android-maven' +group='com.github.pchmn' diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 00000000..0ae68584 --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/couleurwhatever/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/library/src/androidTest/java/com/pchmn/materialchips/ExampleInstrumentedTest.java b/library/src/androidTest/java/com/pchmn/materialchips/ExampleInstrumentedTest.java new file mode 100644 index 00000000..a28c3f32 --- /dev/null +++ b/library/src/androidTest/java/com/pchmn/materialchips/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.pchmn.materialchips; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.pchmn.library.test", appContext.getPackageName()); + } +} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 00000000..633e89cb --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/library/src/main/java/com/pchmn/materialchips/ChipView.java b/library/src/main/java/com/pchmn/materialchips/ChipView.java new file mode 100644 index 00000000..6c6b27bd --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/ChipView.java @@ -0,0 +1,466 @@ +package com.pchmn.materialchips; + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.ColorInt; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.pchmn.materialchips.model.Chip; +import com.pchmn.materialchips.model.ChipInterface; +import com.pchmn.materialchips.util.LetterTileProvider; +import com.pchmn.materialchips.util.ViewUtil; + +import butterknife.BindView; +import butterknife.ButterKnife; +import de.hdodenhof.circleimageview.CircleImageView; + +public class ChipView extends RelativeLayout { + + private static final String TAG = ChipView.class.toString(); + // context + private Context mContext; + // xml elements + @BindView(R2.id.content) LinearLayout mContentLayout; + @BindView(R2.id.icon) CircleImageView mAvatarIconImageView; + @BindView(R2.id.label) TextView mLabelTextView; + @BindView(R2.id.delete_button) ImageButton mDeleteButton; + // attributes + private static final int NONE = -1; + private String mLabel; + private ColorStateList mLabelColor; + private boolean mHasAvatarIcon = false; + private Drawable mAvatarIconDrawable; + private Uri mAvatarIconUri; + private boolean mDeletable = false; + private Drawable mDeleteIcon; + private ColorStateList mDeleteIconColor; + private ColorStateList mBackgroundColor; + // letter tile provider + private LetterTileProvider mLetterTileProvider; + // chip + private ChipInterface mChip; + + public ChipView(Context context) { + super(context); + mContext = context; + init(null); + } + + public ChipView(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(attrs); + } + + /** + * Inflate the view according to attributes + * + * @param attrs the attributes + */ + private void init(AttributeSet attrs) { + // inflate layout + View rootView = inflate(getContext(), R.layout.chip_view, this); + // butter knife + ButterKnife.bind(this, rootView); + // letter tile provider + mLetterTileProvider = new LetterTileProvider(mContext); + + // attributes + if(attrs != null) { + TypedArray a = mContext.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ChipView, + 0, 0); + + try { + // label + mLabel = a.getString(R.styleable.ChipView_label); + mLabelColor = a.getColorStateList(R.styleable.ChipView_labelColor); + // avatar icon + mHasAvatarIcon = a.getBoolean(R.styleable.ChipView_hasAvatarIcon, false); + int avatarIconId = a.getResourceId(R.styleable.ChipView_avatarIcon, NONE); + if(avatarIconId != NONE) mAvatarIconDrawable = ContextCompat.getDrawable(mContext, avatarIconId); + // delete icon + mDeletable = a.getBoolean(R.styleable.ChipView_deletable, false); + mDeleteIconColor = a.getColorStateList(R.styleable.ChipView_deleteIconColor); + int deleteIconId = a.getResourceId(R.styleable.ChipView_deleteIcon, NONE); + if(deleteIconId != NONE) mDeleteIcon = ContextCompat.getDrawable(mContext, deleteIconId); + // background color + mBackgroundColor = a.getColorStateList(R.styleable.ChipView_backgroundColor); + } + finally { + a.recycle(); + } + } + + // inflate + inflateWithAttributes(); + } + + /** + * Inflate the view + */ + private void inflateWithAttributes() { + // label + setLabel(mLabel); + if(mLabelColor != null) + setLabelColor(mLabelColor); + + // avatar + setHasAvatarIcon(mHasAvatarIcon); + if(mAvatarIconUri != null) + setAvatarIcon(mAvatarIconUri); + else if(mAvatarIconDrawable != null) + setAvatarIcon(mAvatarIconDrawable); + else + mAvatarIconImageView.setImageBitmap(mLetterTileProvider.getLetterTile(getLabel())); + + // delete button + setDeletable(mDeletable); + if(mDeleteIcon != null) + setDeleteIcon(mDeleteIcon); + if(mDeleteIconColor != null) + setDeleteIconColor(mDeleteIconColor); + + // background color + if(mBackgroundColor != null) + setChipBackgroundColor(mBackgroundColor); + } + + public void inflate(ChipInterface chip) { + mChip = chip; + + // icon + if(mHasAvatarIcon && mChip.getAvatarUri() != null) + setAvatarIcon(mChip.getAvatarUri()); + else if(mHasAvatarIcon && mChip.getAvatarDrawable() != null) + setAvatarIcon(mChip.getAvatarDrawable()); + else if(mHasAvatarIcon) + mAvatarIconImageView.setImageBitmap(mLetterTileProvider.getLetterTile(chip.getLabel())); + + // label + mLabelTextView.setText(mChip.getLabel()); + } + + /** + * Get label + * + * @return the label + */ + public String getLabel() { + return mLabelTextView.getText().toString(); + } + + /** + * Set label + * + * @param label the label to set + */ + public void setLabel(String label) { + mLabelTextView.setText(label); + } + + /** + * Set label color + * + * @param color the color to set + */ + public void setLabelColor(ColorStateList color) { + mLabelTextView.setTextColor(color); + } + + /** + * Set label color + * + * @param color the color to set + */ + public void setLabelColor(@ColorInt int color) { + mLabelTextView.setTextColor(color); + } + + /** + * Show or hide avatar icon + * + * @param hasAvatarIcon true to show, false to hide + */ + public void setHasAvatarIcon(boolean hasAvatarIcon) { + if(!hasAvatarIcon) { + // hide icon + mAvatarIconImageView.setVisibility(GONE); + // adjust padding + if(mDeleteButton.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, 0, 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, ViewUtil.dpToPx(12), 0); + + } + else { + // show icon + mAvatarIconImageView.setVisibility(VISIBLE); + // adjust padding + if(mDeleteButton.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, 0, 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, ViewUtil.dpToPx(12), 0); + } + } + + /** + * Set avatar icon + * + * @param avatarIcon the icon to set + */ + public void setAvatarIcon(Drawable avatarIcon) { + // set icon + mAvatarIconImageView.setImageDrawable(avatarIcon); + + // show icon + mAvatarIconImageView.setVisibility(VISIBLE); + // adjust padding + if(mDeleteButton.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, 0, 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, ViewUtil.dpToPx(12), 0); + } + + /** + * Set avatar icon + * + * @param avatarUri the uri of the icon to set + */ + public void setAvatarIcon(Uri avatarUri) { + // set icon + mAvatarIconImageView.setImageURI(avatarUri); + + // show icon + mAvatarIconImageView.setVisibility(VISIBLE); + // adjust padding + if(mDeleteButton.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, 0, 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, ViewUtil.dpToPx(12), 0); + } + + /** + * Show or hide delte button + * + * @param deletable true to show, false to hide + */ + public void setDeletable(boolean deletable) { + if(!deletable) { + // hide delete icon + mDeleteButton.setVisibility(GONE); + // adjust padding + if(mAvatarIconImageView.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, ViewUtil.dpToPx(12), 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, ViewUtil.dpToPx(12), 0); + } + else { + // show icon + mDeleteButton.setVisibility(VISIBLE); + // adjust padding + if(mAvatarIconImageView.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, 0, 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, 0, 0); + } + } + + /** + * Set delete icon color + * + * @param color the color to set + */ + public void setDeleteIconColor(ColorStateList color) { + setDeleteIconColor(color.getDefaultColor()); + } + + /** + * Set delete icon color + * + * @param color the color to set + */ + public void setDeleteIconColor(@ColorInt int color) { + // set color + mDeleteButton.getDrawable().mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + + // show icon + mDeleteButton.setVisibility(VISIBLE); + // adjust padding + if(mAvatarIconImageView.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, 0, 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, 0, 0); + } + + /** + * Set delete icon + * + * @param deleteIcon the icon to set + */ + public void setDeleteIcon(Drawable deleteIcon) { + // set icon + mDeleteButton.setImageDrawable(deleteIcon); + + // show icon + mDeleteButton.setVisibility(VISIBLE); + // adjust padding + if(mAvatarIconImageView.getVisibility() == VISIBLE) + mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, 0, 0); + else + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, 0, 0); + } + + /** + * Set background color + * + * @param color the color to set + */ + public void setChipBackgroundColor(ColorStateList color) { + setChipBackgroundColor(color.getDefaultColor()); + } + + /** + * Set background color + * + * @param color the color to set + */ + public void setChipBackgroundColor(@ColorInt int color) { + mContentLayout.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + } + + /** + * Set the chip object + * + * @param chip the chip + */ + public void setChip(ChipInterface chip) { + mChip = chip; + } + + /** + * Set OnClickListener on the delete button + * + * @param onClickListener the OnClickListener + */ + public void setOnDeleteClicked(OnClickListener onClickListener) { + mDeleteButton.setOnClickListener(onClickListener); + } + + /** + * Set OnclickListener on the entire chip + * + * @param onClickListener the OnClickListener + */ + public void setOnChipClicked(OnClickListener onClickListener) { + mContentLayout.setOnClickListener(onClickListener); + } + + /** + * Builder class + */ + public static class Builder { + private Context context; + private String label; + private ColorStateList labelColor; + private boolean hasAvatarIcon = false; + private Uri avatarIconUri; + private Drawable avatarIconDrawable; + private boolean deletable = false; + private Drawable deleteIcon; + private ColorStateList deleteIconColor; + private ColorStateList backgroundColor; + private ChipInterface chip; + + public Builder(Context context) { + this.context = context; + } + + public Builder label(String label) { + this.label = label; + return this; + } + + public Builder labelColor(ColorStateList labelColor) { + this.labelColor = labelColor; + return this; + } + + public Builder hasAvatarIcon(boolean hasAvatarIcon) { + this.hasAvatarIcon = hasAvatarIcon; + return this; + } + + public Builder avatarIcon(Uri avatarUri) { + this.avatarIconUri = avatarUri; + return this; + } + + public Builder avatarIcon(Drawable avatarIcon) { + this.avatarIconDrawable = avatarIcon; + return this; + } + + public Builder deletable(boolean deletable) { + this.deletable = deletable; + return this; + } + + public Builder deleteIcon(Drawable deleteIcon) { + this.deleteIcon = deleteIcon; + return this; + } + + public Builder deleteIconColor(ColorStateList deleteIconColor) { + this.deleteIconColor = deleteIconColor; + return this; + } + + public Builder backgroundColor(ColorStateList backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + public Builder chip(ChipInterface chip) { + this.chip = chip; + this.label = chip.getLabel(); + this.avatarIconDrawable = chip.getAvatarDrawable(); + this.avatarIconUri = chip.getAvatarUri(); + return this; + } + + public ChipView build() { + return newInstance(this); + } + } + + private static ChipView newInstance(Builder builder) { + ChipView chipView = new ChipView(builder.context); + chipView.mLabel = builder.label; + chipView.mLabelColor = builder.labelColor; + chipView.mHasAvatarIcon = builder.hasAvatarIcon; + chipView.mAvatarIconUri = builder.avatarIconUri; + chipView.mAvatarIconDrawable = builder.avatarIconDrawable; + chipView.mDeletable = builder.deletable; + chipView.mDeleteIcon = builder.deleteIcon; + chipView.mDeleteIconColor = builder.deleteIconColor; + chipView.mBackgroundColor = builder.backgroundColor; + chipView.mChip = builder.chip; + chipView.inflateWithAttributes(); + + return chipView; + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/ChipsInput.java b/library/src/main/java/com/pchmn/materialchips/ChipsInput.java new file mode 100644 index 00000000..ba729fd5 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/ChipsInput.java @@ -0,0 +1,362 @@ +package com.pchmn.materialchips; + + +import android.app.Activity; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; +import android.widget.EditText; + +import com.beloo.widget.chipslayoutmanager.ChipsLayoutManager; +import com.pchmn.materialchips.adapter.ChipsAdapter; +import com.pchmn.materialchips.model.Chip; +import com.pchmn.materialchips.model.ChipInterface; +import com.pchmn.materialchips.util.MyWindowCallback; +import com.pchmn.materialchips.util.ViewUtil; +import com.pchmn.materialchips.views.ChipsInputEditText; +import com.pchmn.materialchips.views.DetailedChipView; +import com.pchmn.materialchips.views.FilterableListView; +import com.pchmn.materialchips.views.ScrollViewMaxHeight; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ChipsInput extends ScrollViewMaxHeight { + + private static final String TAG = ChipsInput.class.toString(); + // context + private Context mContext; + // xml element + @BindView(R2.id.chips_recycler) RecyclerView mRecyclerView; + // adapter + private ChipsAdapter mChipsAdapter; + // attributes + private static final int NONE = -1; + private String mHint; + private ColorStateList mHintColor; + private ColorStateList mTextColor; + private int mMaxRows = 2; + private ColorStateList mChipLabelColor; + private boolean mChipHasAvatarIcon = true; + private boolean mChipDeletable = false; + private Drawable mChipDeleteIcon; + private ColorStateList mChipDeleteIconColor; + private ColorStateList mChipBackgroundColor; + private boolean mShowChipDetailed = true; + private ColorStateList mChipDetailedTextColor; + private ColorStateList mChipDetailedDeleteIconColor; + private ColorStateList mChipDetailedBackgroundColor; + private ColorStateList mFilterableListBackgroundColor; + private ColorStateList mFilterableListTextColor; + // chips listener + private List mChipsListenerList = new ArrayList<>(); + private ChipsListener mChipsListener; + // chip list + private List mChipList; + private FilterableListView mFilterableListView; + // chip validator + private ChipValidator mChipValidator; + + public ChipsInput(Context context) { + super(context); + mContext = context; + init(null); + } + + public ChipsInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(attrs); + } + + /** + * Inflate the view according to attributes + * + * @param attrs the attributes + */ + private void init(AttributeSet attrs) { + // inflate layout + View rootView = inflate(getContext(), R.layout.chips_input, this); + // butter knife + ButterKnife.bind(this, rootView); + + // attributes + if(attrs != null) { + TypedArray a = mContext.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ChipsInput, + 0, 0); + + try { + // hint + mHint = a.getString(R.styleable.ChipsInput_hint); + mHintColor = a.getColorStateList(R.styleable.ChipsInput_hintColor); + mTextColor = a.getColorStateList(R.styleable.ChipsInput_textColor); + mMaxRows = a.getInteger(R.styleable.ChipsInput_maxRows, 2); + setMaxHeight(ViewUtil.dpToPx((40 * mMaxRows) + 8)); + //setVerticalScrollBarEnabled(true); + // chip label color + mChipLabelColor = a.getColorStateList(R.styleable.ChipsInput_chip_labelColor); + // chip avatar icon + mChipHasAvatarIcon = a.getBoolean(R.styleable.ChipsInput_chip_hasAvatarIcon, true); + // chip delete icon + mChipDeletable = a.getBoolean(R.styleable.ChipsInput_chip_deletable, false); + mChipDeleteIconColor = a.getColorStateList(R.styleable.ChipsInput_chip_deleteIconColor); + int deleteIconId = a.getResourceId(R.styleable.ChipsInput_chip_deleteIcon, NONE); + if(deleteIconId != NONE) mChipDeleteIcon = ContextCompat.getDrawable(mContext, deleteIconId); + // chip background color + mChipBackgroundColor = a.getColorStateList(R.styleable.ChipsInput_chip_backgroundColor); + // show chip detailed + mShowChipDetailed = a.getBoolean(R.styleable.ChipsInput_showChipDetailed, true); + // chip detailed text color + mChipDetailedTextColor = a.getColorStateList(R.styleable.ChipsInput_chip_detailed_textColor); + mChipDetailedBackgroundColor = a.getColorStateList(R.styleable.ChipsInput_chip_detailed_backgroundColor); + mChipDetailedDeleteIconColor = a.getColorStateList(R.styleable.ChipsInput_chip_detailed_deleteIconColor); + // filterable list + mFilterableListBackgroundColor = a.getColorStateList(R.styleable.ChipsInput_filterable_list_backgroundColor); + mFilterableListTextColor = a.getColorStateList(R.styleable.ChipsInput_filterable_list_textColor); + } + finally { + a.recycle(); + } + } + + // adapter + mChipsAdapter = new ChipsAdapter(mContext, this, mRecyclerView); + ChipsLayoutManager chipsLayoutManager = ChipsLayoutManager.newBuilder(mContext) + .setOrientation(ChipsLayoutManager.HORIZONTAL) + .build(); + mRecyclerView.setLayoutManager(chipsLayoutManager); + mRecyclerView.setNestedScrollingEnabled(false); + mRecyclerView.setAdapter(mChipsAdapter); + + // set window callback + // will hide DetailedOpenView and hide keyboard on touch outside + android.view.Window.Callback mCallBack = ((Activity) mContext).getWindow().getCallback(); + ((Activity) mContext).getWindow().setCallback(new MyWindowCallback(mCallBack, ((Activity) mContext))); + } + + public void addChip(ChipInterface chip) { + mChipsAdapter.addChip(chip); + } + + public void addChip(Object id, Drawable icon, String label, String info) { + Chip chip = new Chip(id, icon, label, info); + mChipsAdapter.addChip(chip); + } + + public void addChip(Drawable icon, String label, String info) { + Chip chip = new Chip(icon, label, info); + mChipsAdapter.addChip(chip); + } + + public void addChip(Object id, Uri iconUri, String label, String info) { + Chip chip = new Chip(id, iconUri, label, info); + mChipsAdapter.addChip(chip); + } + + public void addChip(Uri iconUri, String label, String info) { + Chip chip = new Chip(iconUri, label, info); + mChipsAdapter.addChip(chip); + } + + public void addChip(String label, String info) { + ChipInterface chip = new Chip(label, info); + mChipsAdapter.addChip(chip); + } + + public void removeChip(ChipInterface chip) { + mChipsAdapter.removeChip(chip); + } + + public void removeChipById(Object id) { + mChipsAdapter.removeChipById(id); + } + + public void removeChipByLabel(String label) { + mChipsAdapter.removeChipByLabel(label); + } + + public void removeChipByInfo(String info) { + mChipsAdapter.removeChipByInfo(info); + } + + public ChipView getChipView() { + int padding = ViewUtil.dpToPx(4); + ChipView chipView = new ChipView.Builder(mContext) + .labelColor(mChipLabelColor) + .hasAvatarIcon(mChipHasAvatarIcon) + .deletable(mChipDeletable) + .deleteIcon(mChipDeleteIcon) + .deleteIconColor(mChipDeleteIconColor) + .backgroundColor(mChipBackgroundColor) + .build(); + + chipView.setPadding(padding, padding, padding, padding); + + return chipView; + } + + public ChipsInputEditText getEditText() { + ChipsInputEditText editText = new ChipsInputEditText(mContext); + if(mHintColor != null) + editText.setHintTextColor(mHintColor); + if(mTextColor != null) + editText.setTextColor(mTextColor); + + return editText; + } + + public DetailedChipView getDetailedChipView(ChipInterface chip) { + return new DetailedChipView.Builder(mContext) + .chip(chip) + .textColor(mChipDetailedTextColor) + .backgroundColor(mChipDetailedBackgroundColor) + .deleteIconColor(mChipDetailedDeleteIconColor) + .build(); + } + + public void addChipsListener(ChipsListener chipsListener) { + mChipsListenerList.add(chipsListener); + mChipsListener = chipsListener; + } + + public void onChipAdded(ChipInterface chip, int size) { + for(ChipsListener chipsListener: mChipsListenerList) { + chipsListener.onChipAdded(chip, size); + } + } + + public void onChipRemoved(ChipInterface chip, int size) { + for(ChipsListener chipsListener: mChipsListenerList) { + chipsListener.onChipRemoved(chip, size); + } + } + + public void onTextChanged(CharSequence text) { + if(mChipsListener != null) { + for(ChipsListener chipsListener: mChipsListenerList) { + chipsListener.onTextChanged(text); + } + // show filterable list + if(mFilterableListView != null) { + if(text.length() > 0) + mFilterableListView.filterList(text); + else + mFilterableListView.fadeOut(); + } + } + } + + public List getSelectedChipList() { + return mChipsAdapter.getChipList(); + } + + public String getHint() { + return mHint; + } + + public void setHint(String mHint) { + this.mHint = mHint; + } + + public void setHintColor(ColorStateList mHintColor) { + this.mHintColor = mHintColor; + } + + public void setTextColor(ColorStateList mTextColor) { + this.mTextColor = mTextColor; + } + + public ChipsInput setMaxRows(int mMaxRows) { + this.mMaxRows = mMaxRows; + return this; + } + + public void setChipLabelColor(ColorStateList mLabelColor) { + this.mChipLabelColor = mLabelColor; + } + + public void setChipHasAvatarIcon(boolean mHasAvatarIcon) { + this.mChipHasAvatarIcon = mHasAvatarIcon; + } + + public boolean chipHasAvatarIcon() { + return mChipHasAvatarIcon; + } + + public void setChipDeletable(boolean mDeletable) { + this.mChipDeletable = mDeletable; + } + + public void setChipDeleteIcon(Drawable mDeleteIcon) { + this.mChipDeleteIcon = mDeleteIcon; + } + + public void setChipDeleteIconColor(ColorStateList mDeleteIconColor) { + this.mChipDeleteIconColor = mDeleteIconColor; + } + + public void setChipBackgroundColor(ColorStateList mBackgroundColor) { + this.mChipBackgroundColor = mBackgroundColor; + } + + public ChipsInput setShowChipDetailed(boolean mShowChipDetailed) { + this.mShowChipDetailed = mShowChipDetailed; + return this; + } + + public boolean isShowChipDetailed() { + return mShowChipDetailed; + } + + public void setChipDetailedTextColor(ColorStateList mChipDetailedTextColor) { + this.mChipDetailedTextColor = mChipDetailedTextColor; + } + + public void setChipDetailedDeleteIconColor(ColorStateList mChipDetailedDeleteIconColor) { + this.mChipDetailedDeleteIconColor = mChipDetailedDeleteIconColor; + } + + public void setChipDetailedBackgroundColor(ColorStateList mChipDetailedBackgroundColor) { + this.mChipDetailedBackgroundColor = mChipDetailedBackgroundColor; + } + + public void setFilterableList(List list) { + mChipList = list; + mFilterableListView = new FilterableListView(mContext); + mFilterableListView.build(mChipList, this, mFilterableListBackgroundColor, mFilterableListTextColor); + mChipsAdapter.setFilterableListView(mFilterableListView); + } + + public List getFilterableList() { + return mChipList; + } + + public ChipValidator getChipValidator() { + return mChipValidator; + } + + public void setChipValidator(ChipValidator mChipValidator) { + this.mChipValidator = mChipValidator; + } + + public interface ChipsListener { + void onChipAdded(ChipInterface chip, int newSize); + void onChipRemoved(ChipInterface chip, int newSize); + void onTextChanged(CharSequence text); + } + + public interface ChipValidator { + boolean areEquals(ChipInterface chip1, ChipInterface chip2); + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/adapter/ChipsAdapter.java b/library/src/main/java/com/pchmn/materialchips/adapter/ChipsAdapter.java new file mode 100644 index 00000000..c38ef36d --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/adapter/ChipsAdapter.java @@ -0,0 +1,392 @@ +package com.pchmn.materialchips.adapter; + +import android.content.Context; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.RelativeLayout; + +import com.pchmn.materialchips.ChipView; +import com.pchmn.materialchips.ChipsInput; +import com.pchmn.materialchips.model.ChipInterface; +import com.pchmn.materialchips.views.ChipsInputEditText; +import com.pchmn.materialchips.views.DetailedChipView; +import com.pchmn.materialchips.model.Chip; +import com.pchmn.materialchips.util.ViewUtil; +import com.pchmn.materialchips.views.FilterableListView; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + + +public class ChipsAdapter extends RecyclerView.Adapter { + + private static final String TAG = ChipsAdapter.class.toString(); + private static final int TYPE_EDIT_TEXT = 0; + private static final int TYPE_ITEM = 1; + private Context mContext; + private ChipsInput mChipsInput; + private List mChipList = new ArrayList<>(); + private String mHintLabel; + private ChipsInputEditText mEditText; + private RecyclerView mRecycler; + + public ChipsAdapter(Context context, ChipsInput chipsInput, RecyclerView recycler) { + mContext = context; + mChipsInput = chipsInput; + mRecycler = recycler; + mHintLabel = mChipsInput.getHint(); + mEditText = mChipsInput.getEditText(); + initEditText(); + } + + private class ItemViewHolder extends RecyclerView.ViewHolder { + + private final ChipView chipView; + + ItemViewHolder(View view) { + super(view); + chipView = (ChipView) view; + } + } + + private class EditTextViewHolder extends RecyclerView.ViewHolder { + + private final EditText editText; + + EditTextViewHolder(View view) { + super(view); + editText = (EditText) view; + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if(viewType == TYPE_EDIT_TEXT) + return new EditTextViewHolder(mEditText); + else + return new ItemViewHolder(mChipsInput.getChipView()); + + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { + // edit text + if(position == mChipList.size()) { + if(mChipList.size() == 0) + mEditText.setHint(mHintLabel); + + // auto fit edit text + autofitEditText(); + } + // chip + else if(getItemCount() > 1) { + ItemViewHolder itemViewHolder = (ItemViewHolder) holder; + itemViewHolder.chipView.inflate(getItem(position)); + // handle click + handleClickOnEditText(itemViewHolder.chipView, position); + } + } + + @Override + public int getItemCount() { + return mChipList.size() + 1; + } + + private ChipInterface getItem(int position) { + return mChipList.get(position); + } + + @Override + public int getItemViewType(int position) { + if (position == mChipList.size()) + return TYPE_EDIT_TEXT; + + return TYPE_ITEM; + } + + @Override + public long getItemId(int position) { + return mChipList.get(position).hashCode(); + } + + private void initEditText() { + mEditText.setLayoutParams(new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mEditText.setHint(mHintLabel); + mEditText.setBackgroundResource(android.R.color.transparent); + // prevent fullscreen on landscape + mEditText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + mEditText.setPrivateImeOptions("nm"); + // no suggestion + mEditText.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + + // handle back space + mEditText.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + // backspace + if(event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { + // remove last chip + if(mChipList.size() > 0) + removeChip(mChipList.size() - 1); + } + return false; + } + }); + + // text changed + mEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mChipsInput.onTextChanged(s); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + } + + private void autofitEditText() { + // min width of edit text = 50 dp + ViewGroup.LayoutParams params = mEditText.getLayoutParams(); + params.width = ViewUtil.dpToPx(50); + mEditText.setLayoutParams(params); + + // listen to change in the tree + mEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + // get right of recycler and left of edit text + int right = mRecycler.getRight(); + int left = mEditText.getLeft(); + + // edit text will fill the space + ViewGroup.LayoutParams params = mEditText.getLayoutParams(); + params.width = right - left - ViewUtil.dpToPx(8); + mEditText.setLayoutParams(params); + + // request focus + mEditText.requestFocus(); + + // remove the listener: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + mEditText.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + mEditText.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + } + + }); + } + + private void handleClickOnEditText(ChipView chipView, final int position) { + // delete chip + chipView.setOnDeleteClicked(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeChip(position); + } + }); + + // show detailed chip + if(mChipsInput.isShowChipDetailed()) { + chipView.setOnChipClicked(new View.OnClickListener() { + @Override + public void onClick(View v) { + // get chip position + int[] coord = new int[2]; + v.getLocationInWindow(coord); + + final DetailedChipView detailedChipView = mChipsInput.getDetailedChipView(getItem(position)); + setDetailedChipViewPosition(detailedChipView, coord); + + // delete button + detailedChipView.setOnDeleteClicked(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeChip(position); + detailedChipView.fadeOut(); + } + }); + } + }); + } + } + + private void setDetailedChipViewPosition(DetailedChipView detailedChipView, int[] coord) { + // window width + ViewGroup rootView = (ViewGroup) mRecycler.getRootView(); + int windowWidth = ViewUtil.getWindowWidth(mContext); + + // chip size + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + ViewUtil.dpToPx(300), + ViewUtil.dpToPx(100)); + + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + + // align left window + if(coord[0] <= 0) { + layoutParams.leftMargin = 0; + layoutParams.topMargin = coord[1] - ViewUtil.dpToPx(13); + detailedChipView.alignLeft(); + } + // align right + else if(coord[0] + ViewUtil.dpToPx(300) > windowWidth + ViewUtil.dpToPx(13)) { + layoutParams.leftMargin = windowWidth - ViewUtil.dpToPx(300); + layoutParams.topMargin = coord[1] - ViewUtil.dpToPx(13); + detailedChipView.alignRight(); + } + // same position as chip + else { + layoutParams.leftMargin = coord[0] - ViewUtil.dpToPx(13); + layoutParams.topMargin = coord[1] - ViewUtil.dpToPx(13); + } + + // show view + rootView.addView(detailedChipView, layoutParams); + detailedChipView.fadeIn(); + } + + public void setFilterableListView(FilterableListView filterableListView) { + if(mEditText != null) + mEditText.setFilterableListView(filterableListView); + } + + public void addChip(ChipInterface chip) { + if(!listContains(mChipList, chip)) { + mChipList.add(chip); + // notify listener + mChipsInput.onChipAdded(chip, mChipList.size()); + // hide hint + mEditText.setHint(null); + // reset text + mEditText.setText(null); + // refresh data + notifyItemInserted(mChipList.size()); + } + } + + public void removeChip(ChipInterface chip) { + int position = mChipList.indexOf(chip); + mChipList.remove(position); + // notify listener + notifyItemRangeChanged(position, getItemCount()); + // if 0 chip + if (mChipList.size() == 0) + mEditText.setHint(mHintLabel); + // refresh data + notifyDataSetChanged(); + } + + public void removeChip(int position) { + ChipInterface chip = mChipList.get(position); + // remove contact + mChipList.remove(position); + // notify listener + mChipsInput.onChipRemoved(chip, mChipList.size()); + // if 0 chip + if (mChipList.size() == 0) + mEditText.setHint(mHintLabel); + // refresh data + notifyDataSetChanged(); + } + + public void removeChipById(Object id) { + for (Iterator iter = mChipList.listIterator(); iter.hasNext(); ) { + ChipInterface chip = iter.next(); + if (chip.getId() != null && chip.getId().equals(id)) { + // remove chip + iter.remove(); + // notify listener + mChipsInput.onChipRemoved(chip, mChipList.size()); + } + } + // if 0 chip + if (mChipList.size() == 0) + mEditText.setHint(mHintLabel); + // refresh data + notifyDataSetChanged(); + } + + public void removeChipByLabel(String label) { + for (Iterator iter = mChipList.listIterator(); iter.hasNext(); ) { + ChipInterface chip = iter.next(); + if (chip.getLabel().equals(label)) { + // remove chip + iter.remove(); + // notify listener + mChipsInput.onChipRemoved(chip, mChipList.size()); + } + } + // if 0 chip + if (mChipList.size() == 0) + mEditText.setHint(mHintLabel); + // refresh data + notifyDataSetChanged(); + } + + public void removeChipByInfo(String info) { + for (Iterator iter = mChipList.listIterator(); iter.hasNext(); ) { + ChipInterface chip = iter.next(); + if (chip.getInfo() != null && chip.getInfo().equals(info)) { + // remove chip + iter.remove(); + // notify listener + mChipsInput.onChipRemoved(chip, mChipList.size()); + } + } + // if 0 chip + if (mChipList.size() == 0) + mEditText.setHint(mHintLabel); + // refresh data + notifyDataSetChanged(); + } + + public List getChipList() { + return mChipList; + } + + private boolean listContains(List contactList, ChipInterface chip) { + Log.e(TAG, mChipsInput.getChipValidator() == null ? "null": "not null"); + + if(mChipsInput.getChipValidator() != null) { + for(ChipInterface item: contactList) { + if(mChipsInput.getChipValidator().areEquals(item, chip)) + return true; + } + } + else { + for(ChipInterface item: contactList) { + if(chip.getId() != null && chip.getId().equals(item.getId())) + return true; + if(chip.getLabel().equals(item.getLabel())) + return true; + } + } + + return false; + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/adapter/FilterableAdapter.java b/library/src/main/java/com/pchmn/materialchips/adapter/FilterableAdapter.java new file mode 100644 index 00000000..b5d8903b --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/adapter/FilterableAdapter.java @@ -0,0 +1,275 @@ +package com.pchmn.materialchips.adapter; + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.TextView; + +import com.pchmn.materialchips.ChipsInput; +import com.pchmn.materialchips.R; +import com.pchmn.materialchips.model.ChipInterface; +import com.pchmn.materialchips.util.ColorUtil; +import com.pchmn.materialchips.util.LetterTileProvider; +import com.pchmn.materialchips.util.ViewUtil; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import de.hdodenhof.circleimageview.CircleImageView; + +import static android.view.View.GONE; + +public class FilterableAdapter extends RecyclerView.Adapter implements Filterable { + + private static final String TAG = FilterableAdapter.class.toString(); + // context + private Context mContext; + // list + private List mOriginalList = new ArrayList<>(); + private List mChipList = new ArrayList<>(); + private List mFilteredList = new ArrayList<>(); + private ChipFilter mFilter; + private ChipsInput mChipsInput; + private LetterTileProvider mLetterTileProvider; + private ColorStateList mBackgroundColor; + private ColorStateList mTextColor; + // recycler + private RecyclerView mRecyclerView; + + + public FilterableAdapter(Context context, + RecyclerView recyclerView, + List chipList, + ChipsInput chipsInput, + ColorStateList backgroundColor, + ColorStateList textColor) { + mContext = context; + mRecyclerView = recyclerView; + Collections.sort(chipList, new Comparator() { + @Override + public int compare(ChipInterface o1, ChipInterface o2) { + Collator collator = Collator.getInstance(Locale.getDefault()); + collator.setStrength(Collator.PRIMARY); + return collator.compare(o1.getLabel(), o2.getLabel()); + } + }); + mOriginalList.addAll(chipList); + mChipList.addAll(chipList); + mFilteredList.addAll(chipList); + mLetterTileProvider = new LetterTileProvider(mContext); + mBackgroundColor = backgroundColor; + mTextColor = textColor; + mChipsInput = chipsInput; + + mChipsInput.addChipsListener(new ChipsInput.ChipsListener() { + @Override + public void onChipAdded(ChipInterface chip, int newSize) { + removeChip(chip); + } + + @Override + public void onChipRemoved(ChipInterface chip, int newSize) { + addChip(chip); + } + + @Override + public void onTextChanged(CharSequence text) { + mRecyclerView.scrollToPosition(0); + } + }); + } + + private class ItemViewHolder extends RecyclerView.ViewHolder { + + private CircleImageView mAvatar; + private TextView mLabel; + private TextView mInfo; + + ItemViewHolder(View view) { + super(view); + mAvatar = (CircleImageView) view.findViewById(R.id.avatar); + mLabel = (TextView) view.findViewById(R.id.label); + mInfo = (TextView) view.findViewById(R.id.info); + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(mContext).inflate(R.layout.item_list_filterable, parent, false); + return new ItemViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + ItemViewHolder itemViewHolder = (ItemViewHolder) holder; + final ChipInterface chip = getItem(position); + + // avatar + if(mChipsInput.chipHasAvatarIcon() && chip.getAvatarUri() != null) { + itemViewHolder.mAvatar.setVisibility(View.VISIBLE); + itemViewHolder.mAvatar.setImageURI(chip.getAvatarUri()); + } + else if(mChipsInput.chipHasAvatarIcon() && chip.getAvatarDrawable() != null) { + itemViewHolder.mAvatar.setVisibility(View.VISIBLE); + itemViewHolder.mAvatar.setImageDrawable(chip.getAvatarDrawable()); + } + else if(mChipsInput.chipHasAvatarIcon()) { + itemViewHolder.mAvatar.setVisibility(View.VISIBLE); + itemViewHolder.mAvatar.setImageBitmap(mLetterTileProvider.getLetterTile(chip.getLabel())); + } + else { + itemViewHolder.mAvatar.setVisibility(GONE); + } + + // label + itemViewHolder.mLabel.setText(chip.getLabel()); + + // info + if(chip.getInfo() != null) { + itemViewHolder.mInfo.setVisibility(View.VISIBLE); + itemViewHolder.mInfo.setText(chip.getInfo()); + } + else { + itemViewHolder.mInfo.setVisibility(GONE); + } + + // colors + if(mBackgroundColor != null) + itemViewHolder.itemView.getBackground().setColorFilter(mBackgroundColor.getDefaultColor(), PorterDuff.Mode.SRC_ATOP); + if(mTextColor != null) { + itemViewHolder.mLabel.setTextColor(mTextColor); + itemViewHolder.mInfo.setTextColor(ColorUtil.alpha(mTextColor.getDefaultColor(), 150)); + } + + // onclick + itemViewHolder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(mChipsInput != null) + mChipsInput.addChip(chip); + } + }); + } + + @Override + public int getItemCount() { + return mFilteredList.size(); + } + + private ChipInterface getItem(int position) { + return mFilteredList.get(position); + } + + @Override + public Filter getFilter() { + if(mFilter == null) + mFilter = new ChipFilter(this, mChipList); + return mFilter; + } + + private class ChipFilter extends Filter { + + private FilterableAdapter adapter; + private List originalList; + private List filteredList; + + public ChipFilter(FilterableAdapter adapter, List originalList) { + super(); + this.adapter = adapter; + this.originalList = originalList; + this.filteredList = new ArrayList<>(); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + filteredList.clear(); + FilterResults results = new FilterResults(); + if (constraint.length() == 0) { + filteredList.addAll(originalList); + } else { + final String filterPattern = constraint.toString().toLowerCase().trim(); + for (ChipInterface chip : originalList) { + if (chip.getLabel().toLowerCase().contains(filterPattern)) { + filteredList.add(chip); + } + else if(chip.getInfo() != null && chip.getInfo().toLowerCase().replaceAll("\\s", "").contains(filterPattern)) { + filteredList.add(chip); + } + } + } + + results.values = filteredList; + results.count = filteredList.size(); + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + mFilteredList.clear(); + mFilteredList.addAll((ArrayList) results.values); + notifyDataSetChanged(); + } + } + + private void removeChip(ChipInterface chip) { + int position = mFilteredList.indexOf(chip); + if (position >= 0) + mFilteredList.remove(position); + + position = mChipList.indexOf(chip); + if(position >= 0) + mChipList.remove(position); + + notifyDataSetChanged(); + } + + private void addChip(ChipInterface chip) { + if(contains(chip)) { + mChipList.add(chip); + mFilteredList.add(chip); + // sort original list + Collections.sort(mChipList, new Comparator() { + @Override + public int compare(ChipInterface o1, ChipInterface o2) { + Collator collator = Collator.getInstance(Locale.getDefault()); + collator.setStrength(Collator.PRIMARY); + return collator.compare(o1.getLabel(), o2.getLabel()); + } + }); + // sort filtered list + Collections.sort(mFilteredList, new Comparator() { + @Override + public int compare(ChipInterface o1, ChipInterface o2) { + Collator collator = Collator.getInstance(Locale.getDefault()); + collator.setStrength(Collator.PRIMARY); + return collator.compare(o1.getLabel(), o2.getLabel()); + } + }); + + notifyDataSetChanged(); + } + } + + private boolean contains(ChipInterface chip) { + for(ChipInterface item: mOriginalList) { + if(item.equals(chip)) + return true; + } + return false; + } + + +} diff --git a/library/src/main/java/com/pchmn/materialchips/model/Chip.java b/library/src/main/java/com/pchmn/materialchips/model/Chip.java new file mode 100644 index 00000000..892e9366 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/model/Chip.java @@ -0,0 +1,78 @@ +package com.pchmn.materialchips.model; + + +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public class Chip implements ChipInterface { + + private Object id; + private Uri avatarUri; + private Drawable avatarDrawable; + private String label; + private String info; + + public Chip(@NonNull Object id, @Nullable Uri avatarUri, @NonNull String label, @Nullable String info) { + this.id = id; + this.avatarUri = avatarUri; + this.label = label; + this.info = info; + } + + public Chip(@NonNull Object id, @Nullable Drawable avatarDrawable, @NonNull String label, @Nullable String info) { + this.id = id; + this.avatarDrawable = avatarDrawable; + this.label = label; + this.info = info; + } + + public Chip(@Nullable Uri avatarUri, @NonNull String label, @Nullable String info) { + this.avatarUri = avatarUri; + this.label = label; + this.info = info; + } + + public Chip(@Nullable Drawable avatarDrawable, @NonNull String label, @Nullable String info) { + this.avatarDrawable = avatarDrawable; + this.label = label; + this.info = info; + } + + public Chip(@NonNull Object id, @NonNull String label, @Nullable String info) { + this.id = id; + this.label = label; + this.info = info; + } + + public Chip(@NonNull String label, @Nullable String info) { + this.label = label; + this.info = info; + } + + @Override + public Object getId() { + return id; + } + + @Override + public Uri getAvatarUri() { + return avatarUri; + } + + @Override + public Drawable getAvatarDrawable() { + return avatarDrawable; + } + + @Override + public String getLabel() { + return label; + } + + @Override + public String getInfo() { + return info; + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/model/ChipInterface.java b/library/src/main/java/com/pchmn/materialchips/model/ChipInterface.java new file mode 100644 index 00000000..e942bfb7 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/model/ChipInterface.java @@ -0,0 +1,14 @@ +package com.pchmn.materialchips.model; + + +import android.graphics.drawable.Drawable; +import android.net.Uri; + +public interface ChipInterface { + + Object getId(); + Uri getAvatarUri(); + Drawable getAvatarDrawable(); + String getLabel(); + String getInfo(); +} diff --git a/library/src/main/java/com/pchmn/materialchips/util/ColorUtil.java b/library/src/main/java/com/pchmn/materialchips/util/ColorUtil.java new file mode 100644 index 00000000..c1b118c1 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/util/ColorUtil.java @@ -0,0 +1,38 @@ +package com.pchmn.materialchips.util; + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.util.TypedValue; + +import com.pchmn.materialchips.R; + +public class ColorUtil { + + public static int lighter(int color, float factor) { + int red = (int) ((Color.red(color) * (1 - factor) / 255 + factor) * 255); + int green = (int) ((Color.green(color) * (1 - factor) / 255 + factor) * 255); + int blue = (int) ((Color.blue(color) * (1 - factor) / 255 + factor) * 255); + return Color.argb(Color.alpha(color), red, green, blue); + } + + public static int lighter(ColorStateList color, float factor) { + return lighter(color.getDefaultColor(), factor); + } + + public static int alpha(int color, int alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + public static boolean isColorDark(int color){ + double darkness = 1 - (0.2126*Color.red(color) + 0.7152*Color.green(color) + 0.0722*Color.blue(color))/255; + return darkness >= 0.5; + } + + public static int getThemeAccentColor (final Context context) { + final TypedValue value = new TypedValue (); + context.getTheme ().resolveAttribute (R.attr.colorAccent, value, true); + return value.data; + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/util/LetterTileProvider.java b/library/src/main/java/com/pchmn/materialchips/util/LetterTileProvider.java new file mode 100644 index 00000000..3d0c736d --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/util/LetterTileProvider.java @@ -0,0 +1,214 @@ +package com.pchmn.materialchips.util; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.text.TextPaint; +import android.util.Log; + +import com.pchmn.materialchips.R; + +/** + * Used to create a {@link Bitmap} that contains a letter used in the English + * alphabet or digit, if there is no letter or digit available, a default image + * is shown instead + */ +public class LetterTileProvider { + + /** The number of available tile colors (see R.array.letter_tile_colors) */ + private static final int NUM_OF_TILE_COLORS = 8; + + /** The {@link TextPaint} used to draw the letter onto the tile */ + private final TextPaint mPaint = new TextPaint(); + /** The bounds that enclose the letter */ + private final Rect mBounds = new Rect(); + /** The {@link Canvas} to draw on */ + private final Canvas mCanvas = new Canvas(); + /** The first char of the name being displayed */ + private final char[] mFirstChar = new char[1]; + + /** The background colors of the tile */ + private final TypedArray mColors; + /** The font size used to display the letter */ + private final int mTileLetterFontSize; + /** The default image to display */ + private final Bitmap mDefaultBitmap; + + /** Width */ + private final int mWidth; + /** Height */ + private final int mHeight; + + /** + * Constructor for LetterTileProvider + * + * @param context The {@link Context} to use + */ + public LetterTileProvider(Context context) { + final Resources res = context.getResources(); + + mPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + mPaint.setColor(Color.WHITE); + mPaint.setTextAlign(Paint.Align.CENTER); + mPaint.setAntiAlias(true); + + mColors = res.obtainTypedArray(R.array.letter_tile_colors); + mTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size); + + //mDefaultBitmap = BitmapFactory.decodeResource(res, android.R.drawable.); + mDefaultBitmap = drawableToBitmap(ContextCompat.getDrawable(context, R.drawable.ic_person_white_24dp)); + mWidth = res.getDimensionPixelSize(R.dimen.letter_tile_size); + mHeight = res.getDimensionPixelSize(R.dimen.letter_tile_size); + } + + /** + * @param displayName The name used to create the letter for the tile + * @return A {@link Bitmap} that contains a letter used in the English + * alphabet or digit, if there is no letter or digit available, a + * default image is shown instead + */ + public Bitmap getLetterTile(String displayName) { + if(displayName.length() == 0) + return null; + + final Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); + final char firstChar = displayName.charAt(0); + + final Canvas c = mCanvas; + c.setBitmap(bitmap); + c.drawColor(pickColor(displayName)); + + if (isLetterOrDigit(firstChar)) { + mFirstChar[0] = Character.toUpperCase(firstChar); + mPaint.setTextSize(mTileLetterFontSize); + mPaint.getTextBounds(mFirstChar, 0, 1, mBounds); + c.drawText(mFirstChar, 0, 1, mWidth / 2, mHeight / 2 + + (mBounds.bottom - mBounds.top) / 2, mPaint); + } else { + // (32 - 24) / 2 = 4 + c.drawBitmap(mDefaultBitmap, ViewUtil.dpToPx(4), ViewUtil.dpToPx(4), null); + } + return bitmap; + } + + /** + * @param displayName The name used to create the letter for the tile + * @return A circular {@link Bitmap} that contains a letter used in the English + * alphabet or digit, if there is no letter or digit available, a + * default image is shown instead + */ + public Bitmap getCircularLetterTile(String displayName) { + final Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); + final char firstChar = displayName.charAt(0); + + final Canvas c = mCanvas; + c.setBitmap(bitmap); + c.drawColor(pickColor(displayName)); + + if (isLetterOrDigit(firstChar)) { + mFirstChar[0] = Character.toUpperCase(firstChar); + mPaint.setTextSize(mTileLetterFontSize); + mPaint.getTextBounds(mFirstChar, 0, 1, mBounds); + c.drawText(mFirstChar, 0, 1, mWidth / 2, mHeight / 2 + + (mBounds.bottom - mBounds.top) / 2, mPaint); + } else { + // (32 - 24) / 2 = 4 + c.drawBitmap(mDefaultBitmap, ViewUtil.dpToPx(4), ViewUtil.dpToPx(4), null); + } + return getCircularBitmap(bitmap); + } + + /** + * @param c The char to check + * @return True if c is in the English alphabet or is a digit, + * false otherwise + */ + private static boolean isLetterOrDigit(char c) { + //return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9'; + return Character.isLetterOrDigit(c); + } + + /** + * @param key The key used to generate the tile color + * @return A new or previously chosen color for key used as the + * tile background color + */ + private int pickColor(String key) { + // String.hashCode() is not supposed to change across java versions, so + // this should guarantee the same key always maps to the same color + final int color = Math.abs(key.hashCode()) % NUM_OF_TILE_COLORS; + try { + return mColors.getColor(color, Color.BLACK); + } finally { + // bug with recycler view + //mColors.recycle(); + } + } + + private Bitmap getCircularBitmap(Bitmap bitmap) { + Bitmap output; + + if (bitmap.getWidth() > bitmap.getHeight()) { + output = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + } else { + output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getWidth(), Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + + float r = 0; + + if (bitmap.getWidth() > bitmap.getHeight()) { + r = bitmap.getHeight() / 2; + } else { + r = bitmap.getWidth() / 2; + } + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + canvas.drawCircle(r, r, r, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + return output; + } + + public static Bitmap drawableToBitmap (Drawable drawable) { + Bitmap bitmap = null; + + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if(bitmapDrawable.getBitmap() != null) { + return bitmapDrawable.getBitmap(); + } + } + + if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + +} diff --git a/library/src/main/java/com/pchmn/materialchips/util/MyWindowCallback.java b/library/src/main/java/com/pchmn/materialchips/util/MyWindowCallback.java new file mode 100644 index 00000000..92435080 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/util/MyWindowCallback.java @@ -0,0 +1,173 @@ +package com.pchmn.materialchips.util; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.view.ActionMode; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SearchEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import com.pchmn.materialchips.views.ChipsInputEditText; +import com.pchmn.materialchips.views.DetailedChipView; + +public class MyWindowCallback implements Window.Callback { + + private Window.Callback mLocalCallback; + private Activity mActivity; + + public MyWindowCallback(Window.Callback localCallback, Activity activity) { + mLocalCallback = localCallback; + mActivity = activity; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent keyEvent) { + return mLocalCallback.dispatchKeyEvent(keyEvent); + } + + @Override + public boolean dispatchKeyShortcutEvent(KeyEvent keyEvent) { + return mLocalCallback.dispatchKeyShortcutEvent(keyEvent); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent motionEvent) { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + View v = mActivity.getCurrentFocus(); + if(v instanceof DetailedChipView) { + Rect outRect = new Rect(); + v.getGlobalVisibleRect(outRect); + if (!outRect.contains((int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { + ((DetailedChipView) v).fadeOut(); + } + } + if (v instanceof ChipsInputEditText) { + Rect outRect = new Rect(); + v.getGlobalVisibleRect(outRect); + if (!outRect.contains((int) motionEvent.getRawX(), (int) motionEvent.getRawY()) + && !((ChipsInputEditText) v).isFilterableListVisible()) { + InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } + } + return mLocalCallback.dispatchTouchEvent(motionEvent); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent motionEvent) { + return mLocalCallback.dispatchTrackballEvent(motionEvent); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent motionEvent) { + return mLocalCallback.dispatchGenericMotionEvent(motionEvent); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent accessibilityEvent) { + return mLocalCallback.dispatchPopulateAccessibilityEvent(accessibilityEvent); + } + + @Nullable + @Override + public View onCreatePanelView(int i) { + return mLocalCallback.onCreatePanelView(i); + } + + @Override + public boolean onCreatePanelMenu(int i, Menu menu) { + return mLocalCallback.onCreatePanelMenu(i, menu); + } + + @Override + public boolean onPreparePanel(int i, View view, Menu menu) { + return mLocalCallback.onPreparePanel(i, view, menu); + } + + @Override + public boolean onMenuOpened(int i, Menu menu) { + return mLocalCallback.onMenuOpened(i, menu); + } + + @Override + public boolean onMenuItemSelected(int i, MenuItem menuItem) { + return mLocalCallback.onMenuItemSelected(i, menuItem); + } + + @Override + public void onWindowAttributesChanged(WindowManager.LayoutParams layoutParams) { + mLocalCallback.onWindowAttributesChanged(layoutParams); + } + + @Override + public void onContentChanged() { + mLocalCallback.onContentChanged(); + } + + @Override + public void onWindowFocusChanged(boolean b) { + mLocalCallback.onWindowFocusChanged(b); + } + + @Override + public void onAttachedToWindow() { + mLocalCallback.onAttachedToWindow(); + } + + @Override + public void onDetachedFromWindow() { + mLocalCallback.onDetachedFromWindow(); + } + + @Override + public void onPanelClosed(int i, Menu menu) { + mLocalCallback.onPanelClosed(i, menu); + } + + @Override + public boolean onSearchRequested() { + return mLocalCallback.onSearchRequested(); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public boolean onSearchRequested(SearchEvent searchEvent) { + return mLocalCallback.onSearchRequested(searchEvent); + } + + @Nullable + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { + return mLocalCallback.onWindowStartingActionMode(callback); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Nullable + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int i) { + return mLocalCallback.onWindowStartingActionMode(callback, i); + } + + @Override + public void onActionModeStarted(ActionMode actionMode) { + mLocalCallback.onActionModeStarted(actionMode); + } + + @Override + public void onActionModeFinished(ActionMode actionMode) { + mLocalCallback.onActionModeFinished(actionMode); + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/util/ViewUtil.java b/library/src/main/java/com/pchmn/materialchips/util/ViewUtil.java new file mode 100644 index 00000000..1cae87da --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/util/ViewUtil.java @@ -0,0 +1,81 @@ +package com.pchmn.materialchips.util; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.DisplayMetrics; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.ViewConfiguration; + +public class ViewUtil { + + private static int windowWidthPortrait = 0; + private static int windowWidthLandscape = 0; + + public static int dpToPx(int dp) { + return (int) (dp * Resources.getSystem().getDisplayMetrics().density); + } + + public static int pxToDp(int px) { + return (int) (px / Resources.getSystem().getDisplayMetrics().density); + } + + public static int getWindowWidth(Context context) { + if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + return getWindowWidthPortrait(context); + } + else { + return getWindowWidthLandscape(context); + } + } + + private static int getWindowWidthPortrait(Context context) { + if(windowWidthPortrait == 0) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + windowWidthPortrait = metrics.widthPixels; + } + + return windowWidthPortrait; + } + + private static int getWindowWidthLandscape(Context context) { + if(windowWidthLandscape == 0) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + windowWidthLandscape = metrics.widthPixels; + } + + return windowWidthLandscape; + } + + public static int getNavBarHeight(Context context) { + int result = 0; + boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + + if(!hasMenuKey && !hasBackKey) { + //The device has a navigation bar + Resources resources = context.getResources(); + + int orientation = context.getResources().getConfiguration().orientation; + int resourceId; + if (isTablet(context)){ + resourceId = resources.getIdentifier(orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_height_landscape", "dimen", "android"); + } else { + resourceId = resources.getIdentifier(orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_width", "dimen", "android"); + } + + if (resourceId > 0) { + return context.getResources().getDimensionPixelSize(resourceId); + } + } + return result; + } + + + private static boolean isTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/views/ChipsInputEditText.java b/library/src/main/java/com/pchmn/materialchips/views/ChipsInputEditText.java new file mode 100644 index 00000000..570d68ed --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/views/ChipsInputEditText.java @@ -0,0 +1,31 @@ +package com.pchmn.materialchips.views; + + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; + +public class ChipsInputEditText extends android.support.v7.widget.AppCompatEditText { + + private FilterableListView filterableListView; + + public ChipsInputEditText(Context context) { + super(context); + } + + public ChipsInputEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public boolean isFilterableListVisible() { + return filterableListView.getVisibility() == VISIBLE; + } + + public FilterableListView getFilterableListView() { + return filterableListView; + } + + public void setFilterableListView(FilterableListView filterableListView) { + this.filterableListView = filterableListView; + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/views/DetailedChipView.java b/library/src/main/java/com/pchmn/materialchips/views/DetailedChipView.java new file mode 100644 index 00000000..766a2e92 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/views/DetailedChipView.java @@ -0,0 +1,273 @@ +package com.pchmn.materialchips.views; + +import android.app.Activity; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.net.Uri; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.pchmn.materialchips.R; +import com.pchmn.materialchips.R2; +import com.pchmn.materialchips.model.Chip; +import com.pchmn.materialchips.model.ChipInterface; +import com.pchmn.materialchips.util.ColorUtil; +import com.pchmn.materialchips.util.LetterTileProvider; +import com.pchmn.materialchips.util.MyWindowCallback; +import com.pchmn.materialchips.util.ViewUtil; + +import butterknife.BindView; +import butterknife.ButterKnife; +import de.hdodenhof.circleimageview.CircleImageView; + + +public class DetailedChipView extends RelativeLayout { + + private static final String TAG = DetailedChipView.class.toString(); + // context + private Context mContext; + // xml elements + @BindView(R2.id.content) RelativeLayout mContentLayout; + @BindView(R2.id.avatar_icon) CircleImageView mAvatarIconImageView; + @BindView(R2.id.name) TextView mNameTextView; + @BindView(R2.id.info) TextView mInfoTextView; + @BindView(R2.id.delete_button) ImageButton mDeleteButton; + // letter tile provider + private static LetterTileProvider mLetterTileProvider; + // attributes + private ColorStateList mBackgroundColor; + + public DetailedChipView(Context context) { + super(context); + mContext = context; + init(null); + } + + public DetailedChipView(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(attrs); + } + + /** + * Inflate the view according to attributes + * + * @param attrs the attributes + */ + private void init(AttributeSet attrs) { + // inflate layout + View rootView = inflate(getContext(), R.layout.detailed_chip_view, this); + // butter knife + ButterKnife.bind(this, rootView); + // letter tile provider + mLetterTileProvider = new LetterTileProvider(mContext); + + // hide on first + setVisibility(GONE); + // hide on touch outside + hideOnTouchOutside(); + } + + /** + * Hide the view on touch outside of it + */ + private void hideOnTouchOutside() { + // set focusable + setFocusable(true); + setFocusableInTouchMode(true); + setClickable(true); + } + + /** + * Fade in + */ + public void fadeIn() { + AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(VISIBLE); + // focus on the view + requestFocus(); + } + + /** + * Fade out + */ + public void fadeOut() { + AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(GONE); + // fix onclick issue + clearFocus(); + setClickable(false); + } + + public void setAvatarIcon(Drawable icon) { + mAvatarIconImageView.setImageDrawable(icon); + } + + public void setAvatarIcon(Bitmap icon) { + mAvatarIconImageView.setImageBitmap(icon); + } + + public void setAvatarIcon(Uri icon) { + mAvatarIconImageView.setImageURI(icon); + } + + public void setName(String name) { + mNameTextView.setText(name); + } + + public void setInfo(String info) { + if(info != null) { + mInfoTextView.setVisibility(VISIBLE); + mInfoTextView.setText(info); + } + else { + mInfoTextView.setVisibility(GONE); + } + } + + public void setTextColor(ColorStateList color) { + mNameTextView.setTextColor(color); + mInfoTextView.setTextColor(ColorUtil.alpha(color.getDefaultColor(), 150)); + } + + public void setBackGroundcolor(ColorStateList color) { + mBackgroundColor = color; + mContentLayout.getBackground().setColorFilter(color.getDefaultColor(), PorterDuff.Mode.SRC_ATOP); + } + + public int getBackgroundColor() { + return mBackgroundColor == null ? ContextCompat.getColor(mContext, R.color.colorAccent) : mBackgroundColor.getDefaultColor(); + } + + public void setDeleteIconColor(ColorStateList color) { + mDeleteButton.getDrawable().mutate().setColorFilter(color.getDefaultColor(), PorterDuff.Mode.SRC_ATOP); + } + + public void setOnDeleteClicked(OnClickListener onClickListener) { + mDeleteButton.setOnClickListener(onClickListener); + } + + public void alignLeft() { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mContentLayout.getLayoutParams(); + params.leftMargin = 0; + mContentLayout.setLayoutParams(params); + } + + public void alignRight() { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mContentLayout.getLayoutParams(); + params.rightMargin = 0; + mContentLayout.setLayoutParams(params); + } + + public static class Builder { + private Context context; + private Uri avatarUri; + private Drawable avatarDrawable; + private String name; + private String info; + private ColorStateList textColor; + private ColorStateList backgroundColor; + private ColorStateList deleteIconColor; + + public Builder(Context context) { + this.context = context; + } + + public Builder avatar(Uri avatarUri) { + this.avatarUri = avatarUri; + return this; + } + + public Builder avatar(Drawable avatarDrawable) { + this.avatarDrawable = avatarDrawable; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder info(String info) { + this.info = info; + return this; + } + + public Builder chip(ChipInterface chip) { + this.avatarUri = chip.getAvatarUri(); + this.avatarDrawable = chip.getAvatarDrawable(); + this.name = chip.getLabel(); + this.info = chip.getInfo(); + return this; + } + + public Builder textColor(ColorStateList textColor) { + this.textColor = textColor; + return this; + } + + public Builder backgroundColor(ColorStateList backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + public Builder deleteIconColor(ColorStateList deleteIconColor) { + this.deleteIconColor = deleteIconColor; + return this; + } + + public DetailedChipView build() { + return DetailedChipView.newInstance(this); + } + } + + private static DetailedChipView newInstance(Builder builder) { + DetailedChipView detailedChipView = new DetailedChipView(builder.context); + // avatar + if(builder.avatarUri != null) + detailedChipView.setAvatarIcon(builder.avatarUri); + else if(builder.avatarDrawable != null) + detailedChipView.setAvatarIcon(builder.avatarDrawable); + else + detailedChipView.setAvatarIcon(mLetterTileProvider.getLetterTile(builder.name)); + + // background color + if(builder.backgroundColor != null) + detailedChipView.setBackGroundcolor(builder.backgroundColor); + + // text color + if(builder.textColor != null) + detailedChipView.setTextColor(builder.textColor); + else if(ColorUtil.isColorDark(detailedChipView.getBackgroundColor())) + detailedChipView.setTextColor(ColorStateList.valueOf(Color.WHITE)); + else + detailedChipView.setTextColor(ColorStateList.valueOf(Color.BLACK)); + + // delete icon color + if(builder.deleteIconColor != null) + detailedChipView.setDeleteIconColor(builder.deleteIconColor); + else if(ColorUtil.isColorDark(detailedChipView.getBackgroundColor())) + detailedChipView.setDeleteIconColor(ColorStateList.valueOf(Color.WHITE)); + else + detailedChipView.setDeleteIconColor(ColorStateList.valueOf(Color.BLACK)); + + detailedChipView.setName(builder.name); + detailedChipView.setInfo(builder.info); + return detailedChipView; + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/views/FilterableListView.java b/library/src/main/java/com/pchmn/materialchips/views/FilterableListView.java new file mode 100644 index 00000000..8e94cd52 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/views/FilterableListView.java @@ -0,0 +1,158 @@ +package com.pchmn.materialchips.views; + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.os.Build; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.AlphaAnimation; +import android.widget.Filter; +import android.widget.RelativeLayout; + +import com.pchmn.materialchips.ChipsInput; +import com.pchmn.materialchips.R; +import com.pchmn.materialchips.R2; +import com.pchmn.materialchips.adapter.FilterableAdapter; +import com.pchmn.materialchips.model.ChipInterface; +import com.pchmn.materialchips.util.ViewUtil; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class FilterableListView extends RelativeLayout { + + private static final String TAG = FilterableListView.class.toString(); + private Context mContext; + // list + @BindView(R2.id.recycler_view) RecyclerView mRecyclerView; + private FilterableAdapter mAdapter; + private List mFilterableList; + // others + private ChipsInput mChipsInput; + + public FilterableListView(Context context) { + super(context); + mContext = context; + init(); + } + + private void init() { + // inflate layout + View view = inflate(getContext(), R.layout.list_filterable_view, this); + // butter knife + ButterKnife.bind(this, view); + + // recycler + mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false)); + + // hide on first + setVisibility(GONE); + } + + public void build(List filterableList, ChipsInput chipsInput, ColorStateList backgroundColor, ColorStateList textColor) { + mFilterableList = filterableList; + mChipsInput = chipsInput; + + // adapter + mAdapter = new FilterableAdapter(mContext, mRecyclerView, filterableList, chipsInput, backgroundColor, textColor); + mRecyclerView.setAdapter(mAdapter); + if(backgroundColor != null) + mRecyclerView.getBackground().setColorFilter(backgroundColor.getDefaultColor(), PorterDuff.Mode.SRC_ATOP); + + // listen to change in the tree + mChipsInput.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + + // position + ViewGroup rootView = (ViewGroup) mChipsInput.getRootView(); + + // size + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + ViewUtil.getWindowWidth(mContext), + ViewGroup.LayoutParams.MATCH_PARENT); + + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + + if(mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + layoutParams.bottomMargin = ViewUtil.getNavBarHeight(mContext); + } + + + // add view + rootView.addView(FilterableListView.this, layoutParams); + + // remove the listener: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + mChipsInput.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + mChipsInput.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + } + + }); + } + + public void filterList(CharSequence text) { + mAdapter.getFilter().filter(text, new Filter.FilterListener() { + @Override + public void onFilterComplete(int count) { + // show if there are results + if(mAdapter.getItemCount() > 0) + fadeIn(); + else + fadeOut(); + } + }); + } + + /** + * Fade in + */ + public void fadeIn() { + if(getVisibility() == VISIBLE) + return; + + // get visible window (keyboard shown) + final View rootView = getRootView(); + Rect r = new Rect(); + rootView.getWindowVisibleDisplayFrame(r); + + int[] coord = new int[2]; + mChipsInput.getLocationInWindow(coord); + ViewGroup.MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams(); + layoutParams.topMargin = coord[1] + mChipsInput.getHeight(); + // height of the keyboard + layoutParams.bottomMargin = rootView.getHeight() - r.bottom; + setLayoutParams(layoutParams); + + AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(VISIBLE); + } + + /** + * Fade out + */ + public void fadeOut() { + if(getVisibility() == GONE) + return; + + AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(GONE); + } +} diff --git a/library/src/main/java/com/pchmn/materialchips/views/ScrollViewMaxHeight.java b/library/src/main/java/com/pchmn/materialchips/views/ScrollViewMaxHeight.java new file mode 100644 index 00000000..db212ff6 --- /dev/null +++ b/library/src/main/java/com/pchmn/materialchips/views/ScrollViewMaxHeight.java @@ -0,0 +1,49 @@ +package com.pchmn.materialchips.views; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v4.widget.NestedScrollView; +import android.util.AttributeSet; + +import com.pchmn.materialchips.R; +import com.pchmn.materialchips.util.ViewUtil; + +public class ScrollViewMaxHeight extends NestedScrollView { + + private int mMaxHeight; + private int mWidthMeasureSpec; + + public ScrollViewMaxHeight(Context context) { + super(context); + } + + public ScrollViewMaxHeight(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ScrollViewMaxHeight, + 0, 0); + + try { + mMaxHeight = a.getDimensionPixelSize(R.styleable.ScrollViewMaxHeight_maxHeight, ViewUtil.dpToPx(300)); + } + finally { + a.recycle(); + } + } + + public void setMaxHeight(int height) { + mMaxHeight = height; + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST); + measure(mWidthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mWidthMeasureSpec = widthMeasureSpec; + heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/library/src/main/res/drawable-v21/ripple_chip_view.xml b/library/src/main/res/drawable-v21/ripple_chip_view.xml new file mode 100644 index 00000000..aea2c3a4 --- /dev/null +++ b/library/src/main/res/drawable-v21/ripple_chip_view.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/drawable/avatar.png b/library/src/main/res/drawable/avatar.png new file mode 100644 index 00000000..337f6ead Binary files /dev/null and b/library/src/main/res/drawable/avatar.png differ diff --git a/library/src/main/res/drawable/bg_chip_view.xml b/library/src/main/res/drawable/bg_chip_view.xml new file mode 100644 index 00000000..58b375ec --- /dev/null +++ b/library/src/main/res/drawable/bg_chip_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/drawable/bg_chip_view_opened.xml b/library/src/main/res/drawable/bg_chip_view_opened.xml new file mode 100644 index 00000000..69dbc5bd --- /dev/null +++ b/library/src/main/res/drawable/bg_chip_view_opened.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/drawable/ic_cancel_grey_24dp.xml b/library/src/main/res/drawable/ic_cancel_grey_24dp.xml new file mode 100644 index 00000000..f9b26632 --- /dev/null +++ b/library/src/main/res/drawable/ic_cancel_grey_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/library/src/main/res/drawable/ic_cancel_white_24dp.xml b/library/src/main/res/drawable/ic_cancel_white_24dp.xml new file mode 100644 index 00000000..e6545bf8 --- /dev/null +++ b/library/src/main/res/drawable/ic_cancel_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/library/src/main/res/drawable/ic_person_outline_white_24dp.xml b/library/src/main/res/drawable/ic_person_outline_white_24dp.xml new file mode 100644 index 00000000..69453b4e --- /dev/null +++ b/library/src/main/res/drawable/ic_person_outline_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/library/src/main/res/drawable/ic_person_white_24dp.xml b/library/src/main/res/drawable/ic_person_white_24dp.xml new file mode 100644 index 00000000..22ca1566 --- /dev/null +++ b/library/src/main/res/drawable/ic_person_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/library/src/main/res/drawable/ripple_chip_view.xml b/library/src/main/res/drawable/ripple_chip_view.xml new file mode 100644 index 00000000..58b375ec --- /dev/null +++ b/library/src/main/res/drawable/ripple_chip_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/layout/chip_view.xml b/library/src/main/res/layout/chip_view.xml new file mode 100644 index 00000000..ce7310b2 --- /dev/null +++ b/library/src/main/res/layout/chip_view.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/layout/chips_input.xml b/library/src/main/res/layout/chips_input.xml new file mode 100644 index 00000000..f39d6fc7 --- /dev/null +++ b/library/src/main/res/layout/chips_input.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/library/src/main/res/layout/detailed_chip_view.xml b/library/src/main/res/layout/detailed_chip_view.xml new file mode 100644 index 00000000..3ceed2cc --- /dev/null +++ b/library/src/main/res/layout/detailed_chip_view.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/layout/item_list_filterable.xml b/library/src/main/res/layout/item_list_filterable.xml new file mode 100644 index 00000000..cbe46c5a --- /dev/null +++ b/library/src/main/res/layout/item_list_filterable.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/layout/list_filterable_view.xml b/library/src/main/res/layout/list_filterable_view.xml new file mode 100644 index 00000000..3dc22897 --- /dev/null +++ b/library/src/main/res/layout/list_filterable_view.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml new file mode 100644 index 00000000..871a27ac --- /dev/null +++ b/library/src/main/res/values/attrs.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/values/colors.xml b/library/src/main/res/values/colors.xml new file mode 100644 index 00000000..9f540beb --- /dev/null +++ b/library/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + + #E0E0E0 + #009688 + #ababab + #b9ffffff + ?attr/colorAccent + + \ No newline at end of file diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml new file mode 100644 index 00000000..1a143039 --- /dev/null +++ b/library/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + MaterialChipsInput + + + + + #f16364 + #f58559 + #f9a43e + #e4c62e + #67bf74 + #59a2be + #2093cd + #ad62a7 + + + + 17sp + + 32dp + + diff --git a/library/src/test/java/com/pchmn/materialchips/ExampleUnitTest.java b/library/src/test/java/com/pchmn/materialchips/ExampleUnitTest.java new file mode 100644 index 00000000..b68d9790 --- /dev/null +++ b/library/src/test/java/com/pchmn/materialchips/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.pchmn.materialchips; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 00000000..1882ae03 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.application' +apply plugin: 'me.tatarka.retrolambda' +apply plugin: 'com.jakewharton.butterknife' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "com.pchmn.sample.materialchipsinput" + minSdkVersion 15 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile project(path: ':library') + + // butter knife + compile 'com.android.support:appcompat-v7:25.3.0' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + compile 'io.reactivex.rxjava2:rxjava:2.0.8' + compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.3@aar' + compile 'com.jakewharton:butterknife:8.5.1' + testCompile 'junit:junit:4.12' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 00000000..0ae68584 --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/couleurwhatever/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/sample/src/androidTest/java/com/pchmn/sample/materialchipsinput/ExampleInstrumentedTest.java b/sample/src/androidTest/java/com/pchmn/sample/materialchipsinput/ExampleInstrumentedTest.java new file mode 100644 index 00000000..400aed59 --- /dev/null +++ b/sample/src/androidTest/java/com/pchmn/sample/materialchipsinput/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.pchmn.sample.materialchipsinput; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.pchmn.sample.materialchipsinput", appContext.getPackageName()); + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 00000000..15326c11 --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/com/pchmn/sample/materialchipsinput/ChipExamplesActivity.java b/sample/src/main/java/com/pchmn/sample/materialchipsinput/ChipExamplesActivity.java new file mode 100644 index 00000000..8ba18c73 --- /dev/null +++ b/sample/src/main/java/com/pchmn/sample/materialchipsinput/ChipExamplesActivity.java @@ -0,0 +1,81 @@ +package com.pchmn.sample.materialchipsinput; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; + +import com.pchmn.materialchips.ChipView; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ChipExamplesActivity extends AppCompatActivity { + + private static final String TAG = ChipExamplesActivity.class.toString(); + @BindView(R.id.chip1) ChipView mChip1; + @BindView(R.id.chip2) ChipView mChip2; + @BindView(R.id.chip3) ChipView mChip3; + @BindView(R.id.chip4) ChipView mChip4; + @BindView(R.id.chip5) ChipView mChip5; + @BindView(R.id.chip6) ChipView mChip6; + @BindView(R.id.chip7) ChipView mChip7; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_chip_examples); + // butter knife + ButterKnife.bind(this); + + // chip 1 + mChip1.setOnChipClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip1.getLabel() + ": clicked", Toast.LENGTH_SHORT).show(); + }); + mChip1.setOnDeleteClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip1.getLabel() + ": delete clicked", Toast.LENGTH_SHORT).show(); + }); + + // chip 2 + mChip2.setOnChipClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip2.getLabel() + ": clicked", Toast.LENGTH_SHORT).show(); + }); + + // chip 3 + mChip3.setOnChipClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip3.getLabel() + ": clicked", Toast.LENGTH_SHORT).show(); + }); + mChip3.setOnDeleteClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip3.getLabel() + ": delete clicked", Toast.LENGTH_SHORT).show(); + }); + + // chip 4 + mChip4.setOnChipClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip4.getLabel() + ": clicked", Toast.LENGTH_SHORT).show(); + }); + mChip4.setOnDeleteClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip4.getLabel() + ": delete clicked", Toast.LENGTH_SHORT).show(); + }); + + // chip 5 + mChip5.setOnChipClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip5.getLabel() + ": clicked", Toast.LENGTH_SHORT).show(); + }); + + // chip 6 + mChip6.setOnChipClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip6.getLabel() + ": clicked", Toast.LENGTH_SHORT).show(); + }); + mChip6.setOnDeleteClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip6.getLabel() + ": delete clicked", Toast.LENGTH_SHORT).show(); + }); + + // chip 7 + mChip7.setOnChipClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip7.getLabel() + ": clicked", Toast.LENGTH_SHORT).show(); + }); + mChip7.setOnDeleteClicked(view -> { + Toast.makeText(ChipExamplesActivity.this, mChip7.getLabel() + ": delete clicked", Toast.LENGTH_SHORT).show(); + }); + } +} diff --git a/sample/src/main/java/com/pchmn/sample/materialchipsinput/ContactChip.java b/sample/src/main/java/com/pchmn/sample/materialchipsinput/ContactChip.java new file mode 100644 index 00000000..2706b54d --- /dev/null +++ b/sample/src/main/java/com/pchmn/sample/materialchipsinput/ContactChip.java @@ -0,0 +1,47 @@ +package com.pchmn.sample.materialchipsinput; + + +import android.graphics.drawable.Drawable; +import android.net.Uri; + +import com.pchmn.materialchips.model.ChipInterface; + +public class ContactChip implements ChipInterface { + + private String id; + private Uri avatarUri; + private String name; + private String phoneNumber; + + public ContactChip(String id, Uri avatarUri, String name, String phoneNumber) { + this.id = id; + this.avatarUri = avatarUri; + this.name = name; + this.phoneNumber = phoneNumber; + } + + @Override + public Object getId() { + return id; + } + + @Override + public Uri getAvatarUri() { + return avatarUri; + } + + @Override + public Drawable getAvatarDrawable() { + return null; + } + + @Override + public String getLabel() { + return name; + } + + @Override + public String getInfo() { + return phoneNumber; + } +} diff --git a/sample/src/main/java/com/pchmn/sample/materialchipsinput/ContactListActivity.java b/sample/src/main/java/com/pchmn/sample/materialchipsinput/ContactListActivity.java new file mode 100644 index 00000000..ebdb1b41 --- /dev/null +++ b/sample/src/main/java/com/pchmn/sample/materialchipsinput/ContactListActivity.java @@ -0,0 +1,123 @@ +package com.pchmn.sample.materialchipsinput; + +import android.Manifest; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.pchmn.materialchips.ChipsInput; +import com.pchmn.materialchips.model.Chip; +import com.pchmn.materialchips.model.ChipInterface; +import com.tbruyelle.rxpermissions2.RxPermissions; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ContactListActivity extends AppCompatActivity { + + private static final String TAG = ContactListActivity.class.toString(); + @BindView(R.id.chips_input) ChipsInput mChipsInput; + @BindView(R.id.validate) Button mValidateButton; + @BindView(R.id.chip_list) TextView mChipListText; + private List mContactList = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_contact_list); + // butter knife + ButterKnife.bind(this); + + // get contact list + new RxPermissions(this) + .request(Manifest.permission.READ_CONTACTS) + .subscribe(granted -> { + if(granted && mContactList.size() == 0) + getContactList(); + }); + + // chips listener + mChipsInput.addChipsListener(new ChipsInput.ChipsListener() { + @Override + public void onChipAdded(ChipInterface chip, int newSize) { + Log.e(TAG, "chip added, " + newSize); + } + + @Override + public void onChipRemoved(ChipInterface chip, int newSize) { + Log.e(TAG, "chip removed, " + newSize); + } + + @Override + public void onTextChanged(CharSequence text) { + Log.e(TAG, "text changed: " + text.toString()); + } + }); + + // show selected chips + mValidateButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String listString = ""; + for(ContactChip chip: (List) mChipsInput.getSelectedChipList()) { + listString += chip.getLabel() + " (" + (chip.getInfo() != null ? chip.getInfo(): "") + ")" + ", "; + } + + mChipListText.setText(listString); + } + }); + } + + /** + * Get the contacts of the user and add each contact in the mContactList + * And finally pass the mContactList to the mChipsInput + */ + private void getContactList() { + Cursor phones = this.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null,null,null, null); + + // loop over all contacts + if(phones != null) { + while (phones.moveToNext()) { + // get contact info + String phoneNumber = null; + String id = phones.getString(phones.getColumnIndex(ContactsContract.Contacts._ID)); + String name = phones.getString(phones.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); + String avatarUriString = phones.getString(phones.getColumnIndex(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI)); + Uri avatarUri = null; + if(avatarUriString != null) + avatarUri = Uri.parse(avatarUriString); + + // get phone number + if (Integer.parseInt(phones.getString(phones.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER))) > 0) { + Cursor pCur = this.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", new String[] { id }, null); + + while (pCur != null && pCur.moveToNext()) { + phoneNumber = pCur.getString(pCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); + } + + pCur.close(); + + } + + ContactChip contactChip = new ContactChip(id, avatarUri, name, phoneNumber); + // add contact to the list + mContactList.add(contactChip); + } + phones.close(); + } + + // pass contact list to chips input + mChipsInput.setFilterableList(mContactList); + } +} diff --git a/sample/src/main/java/com/pchmn/sample/materialchipsinput/MainActivity.java b/sample/src/main/java/com/pchmn/sample/materialchipsinput/MainActivity.java new file mode 100644 index 00000000..664a4cf4 --- /dev/null +++ b/sample/src/main/java/com/pchmn/sample/materialchipsinput/MainActivity.java @@ -0,0 +1,32 @@ +package com.pchmn.sample.materialchipsinput; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.widget.Button; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class MainActivity extends AppCompatActivity { + + private static final String TAG = MainActivity.class.toString(); + @BindView(R.id.contacts_button) Button mContactListButton; + @BindView(R.id.custom_chips_button) Button mCustomChipsButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + // butter knife + ButterKnife.bind(this); + + mContactListButton.setOnClickListener(view -> { + startActivity(new Intent(MainActivity.this, ContactListActivity.class)); + }); + + mCustomChipsButton.setOnClickListener(view -> { + startActivity(new Intent(MainActivity.this, ChipExamplesActivity.class)); + }); + } +} diff --git a/sample/src/main/res/drawable/francis.png b/sample/src/main/res/drawable/francis.png new file mode 100644 index 00000000..71d3bb31 Binary files /dev/null and b/sample/src/main/res/drawable/francis.png differ diff --git a/sample/src/main/res/drawable/ic_check_circle_black_24dp.xml b/sample/src/main/res/drawable/ic_check_circle_black_24dp.xml new file mode 100644 index 00000000..1241edab --- /dev/null +++ b/sample/src/main/res/drawable/ic_check_circle_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/sample/src/main/res/drawable/paul.png b/sample/src/main/res/drawable/paul.png new file mode 100644 index 00000000..d3ddc559 Binary files /dev/null and b/sample/src/main/res/drawable/paul.png differ diff --git a/sample/src/main/res/drawable/teresa.png b/sample/src/main/res/drawable/teresa.png new file mode 100644 index 00000000..50736364 Binary files /dev/null and b/sample/src/main/res/drawable/teresa.png differ diff --git a/sample/src/main/res/layout/activity_chip_examples.xml b/sample/src/main/res/layout/activity_chip_examples.xml new file mode 100644 index 00000000..68442c51 --- /dev/null +++ b/sample/src/main/res/layout/activity_chip_examples.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/layout/activity_contact_list.xml b/sample/src/main/res/layout/activity_contact_list.xml new file mode 100644 index 00000000..496a510a --- /dev/null +++ b/sample/src/main/res/layout/activity_contact_list.xml @@ -0,0 +1,31 @@ + + + + + +